Skip to content

Commit

Permalink
Improve SSR hydration performance (#6204)
Browse files Browse the repository at this point in the history
* 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 <jonatan.svennberg@gmail.com>
  • Loading branch information
benmccann and Jonatan Svennberg committed Apr 21, 2021
1 parent f322e3f commit 10e3e3d
Show file tree
Hide file tree
Showing 3 changed files with 47 additions and 8 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand Down
4 changes: 3 additions & 1 deletion src/runtime/internal/Component.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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);
Expand All @@ -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();
}

Expand Down
50 changes: 43 additions & 7 deletions src/runtime/internal/dom.ts
Original file line number Diff line number Diff line change
@@ -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<Node>();

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) {
Expand Down Expand Up @@ -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 = [];
Expand All @@ -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);
}
}

Expand All @@ -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();
}
}

Expand Down

0 comments on commit 10e3e3d

Please sign in to comment.