From c459824a883eca3b8770ae2b671d3e36262b960e Mon Sep 17 00:00:00 2001 From: paoloricciuti Date: Mon, 30 Mar 2026 08:52:13 +0200 Subject: [PATCH] fix: don't clone node if a renderer is available --- .../src/internal/client/dom/template.js | 95 ++++++++++++------- 1 file changed, 61 insertions(+), 34 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/template.js b/packages/svelte/src/internal/client/dom/template.js index a5d0405d0a..d7c57765a4 100644 --- a/packages/svelte/src/internal/client/dom/template.js +++ b/packages/svelte/src/internal/client/dom/template.js @@ -10,7 +10,18 @@ import { create_fragment, create_comment, set_attribute, - merge_text_nodes + merge_text_nodes, + get_last_child, + import_node, + clone_node, + append_child, + insert_before, + set_text_content, + replace_with, + node_type, + query_selector_all, + get_node_value, + node_name } from './operations.js'; import { create_fragment_from_html } from './reconciler.js'; import { active_effect } from '../runtime.js'; @@ -29,6 +40,7 @@ import { REACTION_RAN, TEXT_NODE } from '#client/constants'; +import { renderer } from '../custom-renderer/state.js'; const TEMPLATE_TAG = IS_XHTML ? 'template' : 'TEMPLATE'; const SCRIPT_TAG = IS_XHTML ? 'script' : 'SCRIPT'; @@ -75,12 +87,12 @@ export function from_html(content, flags) { } var clone = /** @type {TemplateNode} */ ( - use_import_node || is_firefox ? document.importNode(node, true) : node.cloneNode(true) + use_import_node || is_firefox ? import_node(node, true) : clone_node(node, true) ); if (is_fragment) { var start = /** @type {TemplateNode} */ (get_first_child(clone)); - var end = /** @type {TemplateNode} */ (clone.lastChild); + var end = /** @type {TemplateNode} */ (get_last_child(clone)); assign_nodes(start, end); } else { @@ -122,20 +134,20 @@ function from_namespace(content, flags, ns = 'svg') { var root = /** @type {Element} */ (get_first_child(fragment)); if (is_fragment) { - node = document.createDocumentFragment(); + node = create_fragment(); while (get_first_child(root)) { - node.appendChild(/** @type {TemplateNode} */ (get_first_child(root))); + append_child(node, /** @type {TemplateNode} */ (get_first_child(root))); } } else { node = /** @type {Element} */ (get_first_child(root)); } } - var clone = /** @type {TemplateNode} */ (node.cloneNode(true)); + var clone = /** @type {TemplateNode} */ (clone_node(node, true)); if (is_fragment) { var start = /** @type {TemplateNode} */ (get_first_child(clone)); - var end = /** @type {TemplateNode} */ (clone.lastChild); + var end = /** @type {TemplateNode} */ (get_last_child(clone)); assign_nodes(start, end); } else { @@ -173,13 +185,13 @@ function fragment_from_tree(structure, ns) { for (var item of structure) { if (typeof item === 'string') { - fragment.append(create_text(item)); + append_child(fragment, create_text(item)); continue; } // if `preserveComments === true`, comments are represented as `['// ']` if (item === undefined || item[0][0] === '/') { - fragment.append(create_comment(item ? item[0].slice(3) : '')); + append_child(fragment, create_comment(item ? item[0].slice(3) : '')); continue; } @@ -195,16 +207,18 @@ function fragment_from_tree(structure, ns) { if (children.length > 0) { var target = - element.nodeName === TEMPLATE_TAG - ? /** @type {HTMLTemplateElement} */ (element).content + node_name(element) === TEMPLATE_TAG + ? // TODO: DOM access + /** @type {HTMLTemplateElement} */ (element).content : element; - target.append( - fragment_from_tree(children, element.nodeName === 'foreignObject' ? undefined : namespace) + append_child( + target, + fragment_from_tree(children, node_name(element) === 'foreignObject' ? undefined : namespace) ); } - fragment.append(element); + append_child(fragment, element); } return fragment; @@ -229,7 +243,10 @@ export function from_tree(structure, flags) { return hydrate_node; } - if (node === undefined) { + // for the custom renderer we skip the cloning and create new nodes every time...a bit less efficient + // but save custom renderers implementors from having to implement importNode or cloneNode + // which can be a pain + if (node === undefined || renderer != null) { const ns = (flags & TEMPLATE_USE_SVG) !== 0 ? NAMESPACE_SVG @@ -242,12 +259,16 @@ export function from_tree(structure, flags) { } var clone = /** @type {TemplateNode} */ ( - use_import_node || is_firefox ? document.importNode(node, true) : node.cloneNode(true) + renderer != null + ? node + : use_import_node || is_firefox + ? import_node(node, true) + : clone_node(node, true) ); if (is_fragment) { var start = /** @type {TemplateNode} */ (get_first_child(clone)); - var end = /** @type {TemplateNode} */ (clone.lastChild); + var end = /** @type {TemplateNode} */ (get_last_child(clone)); assign_nodes(start, end); } else { @@ -275,31 +296,34 @@ function run_scripts(node) { // scripts were SSR'd, in which case they will run if (hydrating) return node; - const is_fragment = node.nodeType === DOCUMENT_FRAGMENT_NODE; + const is_fragment = node_type(node) === DOCUMENT_FRAGMENT_NODE; const scripts = - /** @type {HTMLElement} */ (node).nodeName === SCRIPT_TAG + // TODO RENDERER: figure out what to do here + node_name(node) === SCRIPT_TAG ? [/** @type {HTMLScriptElement} */ (node)] - : node.querySelectorAll('script'); + : query_selector_all(node, 'script'); const effect = /** @type {Effect & { nodes: EffectNodes }} */ (active_effect); for (const script of scripts) { const clone = create_element('script'); + // TODO: DOM access for (var attribute of script.attributes) { - clone.setAttribute(attribute.name, attribute.value); + set_attribute(clone, attribute.name, attribute.value); } - clone.textContent = script.textContent; + // TODO: DOM access + set_text_content(clone, script.textContent ?? ''); // The script has changed - if it's at the edges, the effect now points at dead nodes - if (is_fragment ? node.firstChild === script : node === script) { + if (is_fragment ? get_first_child(node) === script : node === script) { effect.nodes.start = clone; } - if (is_fragment ? node.lastChild === script : node === script) { + if (is_fragment ? get_last_child(node) === script : node === script) { effect.nodes.end = clone; } - script.replaceWith(clone); + replace_with(script, clone); } return node; } @@ -317,9 +341,9 @@ export function text(value = '') { var node = hydrate_node; - if (node.nodeType !== TEXT_NODE) { + if (node_type(node) !== TEXT_NODE) { // if an {expression} is empty during SSR, we need to insert an empty text node - node.before((node = create_text())); + insert_before(node, (node = create_text())); set_hydrate_node(node); } else { merge_text_nodes(/** @type {Text} */ (node)); @@ -339,10 +363,11 @@ export function comment() { return hydrate_node; } - var frag = document.createDocumentFragment(); - var start = document.createComment(''); + var frag = create_fragment(); + var start = create_comment(''); var anchor = create_text(); - frag.append(start, anchor); + append_child(frag, start); + append_child(frag, anchor); assign_nodes(start, anchor); @@ -375,24 +400,26 @@ export function append(anchor, dom) { return; } - anchor.before(/** @type {Node} */ (dom)); + insert_before(anchor, /** @type {Node} */ (dom)); } /** * Create (or hydrate) an unique UID for the component instance. */ export function props_id() { + let node_value; if ( hydrating && hydrate_node && - hydrate_node.nodeType === COMMENT_NODE && - hydrate_node.textContent?.startsWith(`$`) + node_type(hydrate_node) === COMMENT_NODE && + (node_value = get_node_value(hydrate_node))?.startsWith(`$`) ) { - const id = hydrate_node.textContent.substring(1); + const id = node_value.substring(1); hydrate_next(); return id; } + // TODO RENDERER: figure out what to do here // @ts-expect-error This way we ensure the id is unique even across Svelte runtimes (window.__svelte ??= {}).uid ??= 1;