From 10e3e3dae8dbd0bf3151c162949b8bf76dc62e8b Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Wed, 21 Apr 2021 09:42:52 -0700 Subject: [PATCH 1/2] Improve SSR hydration performance (#6204) * Improve SSR hydration performance - Fixes #4308 by avoiding de- and reattaching nodes during hydration - Turns existing append, insert and detach methods into "upserts" 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. Co-authored-by: Jonatan Svennberg --- CHANGELOG.md | 1 + src/runtime/internal/Component.ts | 4 ++- src/runtime/internal/dom.ts | 50 ++++++++++++++++++++++++++----- 3 files changed, 47 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb22c97eb7..85281d4326 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 32de46506a..a191e5d83b 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 8d99234c9d..911f96a275 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(); } } From 27234275c0e4534c8f1f3b1be02fb354e88f8ebf Mon Sep 17 00:00:00 2001 From: Mauricio Paternina <49292621+spaceinvadev@users.noreply.github.com> Date: Wed, 21 Apr 2021 11:46:00 -0500 Subject: [PATCH 2/2] tutorial: fix custombutton colors for a11y contrast (#6212) --- .../06-dom-event-forwarding/app-a/CustomButton.svelte | 10 +++++----- .../06-dom-event-forwarding/app-b/CustomButton.svelte | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/site/content/tutorial/05-events/06-dom-event-forwarding/app-a/CustomButton.svelte b/site/content/tutorial/05-events/06-dom-event-forwarding/app-a/CustomButton.svelte index 0955930b5d..e68b9d46d9 100644 --- a/site/content/tutorial/05-events/06-dom-event-forwarding/app-a/CustomButton.svelte +++ b/site/content/tutorial/05-events/06-dom-event-forwarding/app-a/CustomButton.svelte @@ -2,18 +2,18 @@ button { height: 4rem; width: 8rem; - background-color: #aaa; - border-color: #f1c40f; - color: #f1c40f; + background-color: #ddd; + border-color: #ff3e00; + color: #ff3e00; font-size: 1.25rem; - background-image: linear-gradient(45deg, #f1c40f 50%, transparent 50%); + background-image: linear-gradient(45deg, #ff3e00 50%, transparent 50%); background-position: 100%; background-size: 400%; transition: background 300ms ease-in-out; } button:hover { background-position: 0; - color: #aaa; + color: #ddd; } diff --git a/site/content/tutorial/05-events/06-dom-event-forwarding/app-b/CustomButton.svelte b/site/content/tutorial/05-events/06-dom-event-forwarding/app-b/CustomButton.svelte index f521c1f471..3ae3172c9d 100644 --- a/site/content/tutorial/05-events/06-dom-event-forwarding/app-b/CustomButton.svelte +++ b/site/content/tutorial/05-events/06-dom-event-forwarding/app-b/CustomButton.svelte @@ -2,18 +2,18 @@ button { height: 4rem; width: 8rem; - background-color: #aaa; - border-color: #f1c40f; - color: #f1c40f; + background-color: #ddd; + border-color: #ff3e00; + color: #ff3e00; font-size: 1.25rem; - background-image: linear-gradient(45deg, #f1c40f 50%, transparent 50%); + background-image: linear-gradient(45deg, #ff3e00 50%, transparent 50%); background-position: 100%; background-size: 400%; transition: background 300ms ease-in-out; } button:hover { background-position: 0; - color: #aaa; + color: #ddd; }