From ce23b0d56853421da24d769d54fe3e4d78ed7427 Mon Sep 17 00:00:00 2001 From: Jonatan Svennberg Date: Sat, 31 Oct 2020 00:08:52 +0100 Subject: [PATCH] Improve SSR hydration performance and logging - Fixes #4308 by avoiding de- and reattaching nodes during hydration - Turns existing append, insert and detach methods into "upserts" - Logs hydration errors to the console The new "hydration mode" was added in order to maintain the detach by default behavior during hydration. By tracking which nodes are claimed during hydration unclaimed nodes can then removed from the DOM at the end of hydration without touching the remaining nodes. --- src/runtime/internal/Component.ts | 4 ++- src/runtime/internal/dom.ts | 56 +++++++++++++++++++++++++------ 2 files changed, 49 insertions(+), 11 deletions(-) diff --git a/src/runtime/internal/Component.ts b/src/runtime/internal/Component.ts index 459a78031a..31cf7b372e 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 { @@ -147,6 +147,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); @@ -158,6 +159,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); + end_hydrating(); flush(); } diff --git a/src/runtime/internal/dom.ts b/src/runtime/internal/dom.ts index ad06d6ff08..903553ca4c 100644 --- a/src/runtime/internal/dom.ts +++ b/src/runtime/internal/dom.ts @@ -1,15 +1,41 @@ import { has_prop } from './utils'; +let is_hydrating = true; +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); + 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); + 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 +180,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 +195,14 @@ 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 { + // Ignore hydration errors caused by empty text nodes + if (node.nodeType !== 3 || !node.data.match(/\s+/)) { + console.error(`Hydration error: Expected node "${name}" but found`, node); + } + detach(node); } } @@ -176,14 +210,16 @@ export function claim_element(nodes, name, attributes, svg) { } export function claim_text(nodes, data) { - for (let i = 0; i < nodes.length; i += 1) { - const node = nodes[i]; + const node = nodes.shift(); + if (node) { if (node.nodeType === 3) { node.data = '' + data; - return nodes.splice(i, 1)[0]; + return node; + } else { + console.error(`Hydration error: Expected text node "${data}" but found`, node); + detach(node); } } - return text(data); }