diff --git a/CHANGELOG.md b/CHANGELOG.md index eb22c97eb724..85281d4326d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased +* Avoid recreating DOM elements during hydration ([#6204](https://github.com/sveltejs/svelte/pull/6204)) * Add missing function overload for `derived` to allow explicitly setting an initial value for non-async derived stores ([#6172](https://github.com/sveltejs/svelte/pull/6172)) * Pass full markup source to script/style preprocessors ([#6169](https://github.com/sveltejs/svelte/pull/6169)) diff --git a/src/runtime/internal/Component.ts b/src/runtime/internal/Component.ts index 32de46506a27..a191e5d83bca 100644 --- a/src/runtime/internal/Component.ts +++ b/src/runtime/internal/Component.ts @@ -1,7 +1,7 @@ import { add_render_callback, flush, schedule_update, dirty_components } from './scheduler'; import { current_component, set_current_component } from './lifecycle'; import { blank_object, is_empty, is_function, run, run_all, noop } from './utils'; -import { children, detach } from './dom'; +import { children, detach, start_hydrating, end_hydrating } from './dom'; import { transition_in } from './transitions'; interface Fragment { @@ -150,6 +150,7 @@ export function init(component, options, instance, create_fragment, not_equal, p if (options.target) { if (options.hydrate) { + start_hydrating(); const nodes = children(options.target); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion $$.fragment && $$.fragment!.l(nodes); @@ -161,6 +162,7 @@ export function init(component, options, instance, create_fragment, not_equal, p if (options.intro) transition_in(component.$$.fragment); mount_component(component, options.target, options.anchor, options.customElement); + end_hydrating(); flush(); } diff --git a/src/runtime/internal/dom.ts b/src/runtime/internal/dom.ts index 8d99234c9de2..911f96a275ae 100644 --- a/src/runtime/internal/dom.ts +++ b/src/runtime/internal/dom.ts @@ -1,15 +1,47 @@ import { has_prop } from './utils'; +// Track which nodes are claimed during hydration. Unclaimed nodes can then be removed from the DOM +// at the end of hydration without touching the remaining nodes. +let is_hydrating = false; +const nodes_to_detach = new Set(); + +export function start_hydrating() { + is_hydrating = true; +} +export function end_hydrating() { + is_hydrating = false; + + for (const node of nodes_to_detach) { + node.parentNode.removeChild(node); + } + + nodes_to_detach.clear(); +} + export function append(target: Node, node: Node) { - target.appendChild(node); + if (is_hydrating) { + nodes_to_detach.delete(node); + } + if (node.parentNode !== target) { + target.appendChild(node); + } } export function insert(target: Node, node: Node, anchor?: Node) { - target.insertBefore(node, anchor || null); + if (is_hydrating) { + nodes_to_detach.delete(node); + } + if (node.parentNode !== target || (anchor && node.nextSibling !== anchor)) { + target.insertBefore(node, anchor || null); + } } export function detach(node: Node) { - node.parentNode.removeChild(node); + if (is_hydrating) { + nodes_to_detach.add(node); + } else if (node.parentNode) { + node.parentNode.removeChild(node); + } } export function destroy_each(iterations, detaching) { @@ -154,8 +186,9 @@ export function children(element) { } export function claim_element(nodes, name, attributes, svg) { - for (let i = 0; i < nodes.length; i += 1) { - const node = nodes[i]; + while (nodes.length > 0) { + const node = nodes.shift(); + if (node.nodeName === name) { let j = 0; const remove = []; @@ -168,7 +201,10 @@ export function claim_element(nodes, name, attributes, svg) { for (let k = 0; k < remove.length; k++) { node.removeAttribute(remove[k]); } - return nodes.splice(i, 1)[0]; + + return node; + } else { + detach(node); } } @@ -180,7 +216,7 @@ export function claim_text(nodes, data) { const node = nodes[i]; if (node.nodeType === 3) { node.data = '' + data; - return nodes.splice(i, 1)[0]; + return nodes.shift(); } }