Skip to content

Commit e626d59

Browse files
committed
Implement new hydration optimization
During hydration, greedily pick nodes that exist in the original HTML that should not be detached. Detach the rest.
1 parent e6c2c8e commit e626d59

File tree

2 files changed

+96
-29
lines changed

2 files changed

+96
-29
lines changed

src/runtime/internal/Component.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { add_render_callback, flush, schedule_update, dirty_components } from './scheduler';
22
import { current_component, set_current_component } from './lifecycle';
33
import { blank_object, is_empty, is_function, run, run_all, noop } from './utils';
4-
import { children, detach } from './dom';
4+
import { children, detach, start_hydrating, end_hydrating } from './dom';
55
import { transition_in } from './transitions';
66

77
interface Fragment {
@@ -150,6 +150,7 @@ export function init(component, options, instance, create_fragment, not_equal, p
150150

151151
if (options.target) {
152152
if (options.hydrate) {
153+
start_hydrating();
153154
const nodes = children(options.target);
154155
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
155156
$$.fragment && $$.fragment!.l(nodes);
@@ -161,6 +162,7 @@ export function init(component, options, instance, create_fragment, not_equal, p
161162

162163
if (options.intro) transition_in(component.$$.fragment);
163164
mount_component(component, options.target, options.anchor, options.customElement);
165+
end_hydrating();
164166
flush();
165167
}
166168

src/runtime/internal/dom.ts

Lines changed: 93 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,38 @@
11
import { has_prop } from './utils';
22

3-
export function append(target: Node, node: Node) {
4-
target.appendChild(node);
3+
// Track which nodes are claimed during hydration. Unclaimed nodes can then be removed from the DOM
4+
// at the end of hydration without touching the remaining nodes.
5+
let is_hydrating = false;
6+
7+
export function start_hydrating() {
8+
is_hydrating = true;
9+
}
10+
export function end_hydrating() {
11+
is_hydrating = false;
12+
}
13+
14+
export function append(target: Node & {actual_end_child?: Node | null}, node: Node) {
15+
if (is_hydrating) {
16+
// If we are just starting with this target, we will insert before the firstChild (which may be null)
17+
if (target.actual_end_child === undefined) {
18+
target.actual_end_child = target.firstChild;
19+
}
20+
if (node.parentNode !== target) {
21+
target.insertBefore(node, target.actual_end_child);
22+
} else {
23+
target.actual_end_child = node.nextSibling;
24+
}
25+
} else if (node.parentNode !== target) {
26+
target.appendChild(node);
27+
}
528
}
629

730
export function insert(target: Node, node: Node, anchor?: Node) {
8-
target.insertBefore(node, anchor || null);
31+
if (is_hydrating && !anchor) {
32+
append(target, node);
33+
} else if (node.parentNode !== target || (anchor && node.nextSibling !== anchor)) {
34+
target.insertBefore(node, anchor || null);
35+
}
936
}
1037

1138
export function detach(node: Node) {
@@ -149,42 +176,80 @@ export function time_ranges_to_array(ranges) {
149176
return array;
150177
}
151178

152-
export function children(element) {
179+
export function children(element: HTMLElement) {
153180
return Array.from(element.childNodes);
154181
}
155182

156-
export function claim_element(nodes, name, attributes, svg) {
157-
for (let i = 0; i < nodes.length; i += 1) {
183+
type ChildNodeArray = ChildNode[] & {
184+
/**
185+
* All nodes at or after this index are available for preservation (not getting detached)
186+
*/
187+
lastKeepIndex?: number;
188+
};
189+
190+
function claim_node<R extends ChildNode>(nodes: ChildNodeArray, predicate: (node: ChildNode) => node is R, processNode: (node: ChildNode) => void, createNode: () => R) {
191+
if (nodes.lastKeepIndex === undefined) {
192+
nodes.lastKeepIndex = 0;
193+
}
194+
195+
// We first try to find a node we can actually keep without detaching
196+
// This node should be after the previous node that we chose to keep without detaching
197+
for (let i = nodes.lastKeepIndex; i < nodes.length; i++) {
198+
const node = nodes[i];
199+
200+
if (predicate(node)) {
201+
processNode(node);
202+
203+
nodes.splice(i, 1);
204+
nodes.lastKeepIndex = i;
205+
return node;
206+
}
207+
}
208+
209+
210+
// Otherwise, we try to find a node that we should detach
211+
for (let i = 0; i < nodes.lastKeepIndex; i++) {
158212
const node = nodes[i];
159-
if (node.nodeName === name) {
160-
let j = 0;
213+
214+
if (predicate(node)) {
215+
processNode(node);
216+
217+
nodes.splice(i, 1);
218+
nodes.lastKeepIndex -= 1;
219+
detach(node);
220+
return node;
221+
}
222+
}
223+
224+
// If we can't find any matching node, we create a new one
225+
return createNode();
226+
}
227+
228+
export function claim_element(nodes: ChildNodeArray, name: string, attributes: {[key: string]: boolean}, svg) {
229+
return claim_node<Element | SVGElement>(
230+
nodes,
231+
(node: ChildNode): node is Element | SVGElement => node.nodeName === name,
232+
(node: Element) => {
161233
const remove = [];
162-
while (j < node.attributes.length) {
163-
const attribute = node.attributes[j++];
234+
for (let j = 0; j < node.attributes.length; j++) {
235+
const attribute = node.attributes[j];
164236
if (!attributes[attribute.name]) {
165237
remove.push(attribute.name);
166238
}
167239
}
168-
for (let k = 0; k < remove.length; k++) {
169-
node.removeAttribute(remove[k]);
170-
}
171-
return nodes.splice(i, 1)[0];
172-
}
173-
}
174-
175-
return svg ? svg_element(name) : element(name);
240+
remove.forEach(v => node.removeAttribute(v));
241+
},
242+
() => svg ? svg_element(name as keyof SVGElementTagNameMap) : element(name as keyof HTMLElementTagNameMap)
243+
);
176244
}
177245

178-
export function claim_text(nodes, data) {
179-
for (let i = 0; i < nodes.length; i += 1) {
180-
const node = nodes[i];
181-
if (node.nodeType === 3) {
182-
node.data = '' + data;
183-
return nodes.splice(i, 1)[0];
184-
}
185-
}
186-
187-
return text(data);
246+
export function claim_text(nodes: ChildNodeArray, data) {
247+
return claim_node<Text>(
248+
nodes,
249+
(node: ChildNode): node is Text => node.nodeType === 3,
250+
(node: Text) => node.data = '' + data,
251+
() => text(data)
252+
);
188253
}
189254

190255
export function claim_space(nodes) {

0 commit comments

Comments
 (0)