Skip to content

Commit ecbd96a

Browse files
authored
[fix] hydration improvements (#6449)
1 parent 16d562f commit ecbd96a

File tree

6 files changed

+88
-27
lines changed

6 files changed

+88
-27
lines changed

src/compiler/compile/render_dom/wrappers/Element/Attribute.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ export default class AttributeWrapper extends BaseAttributeWrapper {
132132
`);
133133
} else if (this.is_src) {
134134
block.chunks.hydrate.push(
135-
b`if (${element.var}.src !== ${init}) ${method}(${element.var}, "${name}", ${this.last});`
135+
b`if (!@src_url_equal(${element.var}.src, ${init})) ${method}(${element.var}, "${name}", ${this.last});`
136136
);
137137
updater = b`${method}(${element.var}, "${name}", ${should_cache ? this.last : value});`;
138138
} else if (property_name) {
@@ -193,7 +193,7 @@ export default class AttributeWrapper extends BaseAttributeWrapper {
193193

194194
if (should_cache) {
195195
condition = this.is_src
196-
? x`${condition} && (${element.var}.src !== (${last} = ${value}))`
196+
? x`${condition} && (!@src_url_equal(${element.var}.src, (${last} = ${value})))`
197197
: x`${condition} && (${last} !== (${last} = ${value}))`;
198198
}
199199

src/runtime/internal/dom.ts

+64-15
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export function end_hydrating() {
1414
type NodeEx = Node & {
1515
claim_order?: number,
1616
hydrate_init? : true,
17-
actual_end_child?: Node,
17+
actual_end_child?: NodeEx,
1818
childNodes: NodeListOf<NodeEx>,
1919
};
2020

@@ -37,8 +37,20 @@ function init_hydrate(target: NodeEx) {
3737

3838
type NodeEx2 = NodeEx & {claim_order: number};
3939

40-
// We know that all children have claim_order values since the unclaimed have been detached
41-
const children = target.childNodes as NodeListOf<NodeEx2>;
40+
// We know that all children have claim_order values since the unclaimed have been detached if target is not <head>
41+
let children: ArrayLike<NodeEx2> = target.childNodes as NodeListOf<NodeEx2>;
42+
43+
// If target is <head>, there may be children without claim_order
44+
if (target.nodeName === 'HEAD') {
45+
const myChildren = [];
46+
for (let i = 0; i < children.length; i++) {
47+
const node = children[i];
48+
if (node.claim_order !== undefined) {
49+
myChildren.push(node);
50+
}
51+
}
52+
children = myChildren;
53+
}
4254

4355
/*
4456
* Reorder claimed children optimally.
@@ -70,7 +82,8 @@ function init_hydrate(target: NodeEx) {
7082
// Find the largest subsequence length such that it ends in a value less than our current value
7183

7284
// upper_bound returns first greater value, so we subtract one
73-
const seqLen = upper_bound(1, longest + 1, idx => children[m[idx]].claim_order, current) - 1;
85+
// with fast path for when we are on the current longest subsequence
86+
const seqLen = ((longest > 0 && children[m[longest]].claim_order <= current) ? longest + 1 : upper_bound(1, longest, idx => children[m[idx]].claim_order, current)) - 1;
7487

7588
p[i] = m[seqLen] + 1;
7689

@@ -119,8 +132,17 @@ export function append(target: NodeEx, node: NodeEx) {
119132
if ((target.actual_end_child === undefined) || ((target.actual_end_child !== null) && (target.actual_end_child.parentElement !== target))) {
120133
target.actual_end_child = target.firstChild;
121134
}
135+
136+
// Skip nodes of undefined ordering
137+
while ((target.actual_end_child !== null) && (target.actual_end_child.claim_order === undefined)) {
138+
target.actual_end_child = target.actual_end_child.nextSibling;
139+
}
140+
122141
if (node !== target.actual_end_child) {
123-
target.insertBefore(node, target.actual_end_child);
142+
// We only insert if the ordering of this node should be modified or the parent node is not target
143+
if (node.claim_order !== undefined || node.parentNode !== target) {
144+
target.insertBefore(node, target.actual_end_child);
145+
}
124146
} else {
125147
target.actual_end_child = node.nextSibling;
126148
}
@@ -304,21 +326,29 @@ export function children(element: Element) {
304326
return Array.from(element.childNodes);
305327
}
306328

307-
function claim_node<R extends ChildNodeEx>(nodes: ChildNodeArray, predicate: (node: ChildNodeEx) => node is R, processNode: (node: ChildNodeEx) => void, createNode: () => R, dontUpdateLastIndex: boolean = false) {
308-
// Try to find nodes in an order such that we lengthen the longest increasing subsequence
329+
function init_claim_info(nodes: ChildNodeArray) {
309330
if (nodes.claim_info === undefined) {
310331
nodes.claim_info = {last_index: 0, total_claimed: 0};
311332
}
333+
}
334+
335+
function claim_node<R extends ChildNodeEx>(nodes: ChildNodeArray, predicate: (node: ChildNodeEx) => node is R, processNode: (node: ChildNodeEx) => ChildNodeEx | undefined, createNode: () => R, dontUpdateLastIndex: boolean = false) {
336+
// Try to find nodes in an order such that we lengthen the longest increasing subsequence
337+
init_claim_info(nodes);
312338

313339
const resultNode = (() => {
314340
// We first try to find an element after the previous one
315341
for (let i = nodes.claim_info.last_index; i < nodes.length; i++) {
316342
const node = nodes[i];
317343

318344
if (predicate(node)) {
319-
processNode(node);
345+
const replacement = processNode(node);
320346

321-
nodes.splice(i, 1);
347+
if (replacement === undefined) {
348+
nodes.splice(i, 1);
349+
} else {
350+
nodes[i] = replacement;
351+
}
322352
if (!dontUpdateLastIndex) {
323353
nodes.claim_info.last_index = i;
324354
}
@@ -333,12 +363,16 @@ function claim_node<R extends ChildNodeEx>(nodes: ChildNodeArray, predicate: (no
333363
const node = nodes[i];
334364

335365
if (predicate(node)) {
336-
processNode(node);
366+
const replacement = processNode(node);
337367

338-
nodes.splice(i, 1);
368+
if (replacement === undefined) {
369+
nodes.splice(i, 1);
370+
} else {
371+
nodes[i] = replacement;
372+
}
339373
if (!dontUpdateLastIndex) {
340374
nodes.claim_info.last_index = i;
341-
} else {
375+
} else if (replacement === undefined) {
342376
// Since we spliced before the last_index, we decrease it
343377
nodes.claim_info.last_index--;
344378
}
@@ -368,6 +402,7 @@ export function claim_element(nodes: ChildNodeArray, name: string, attributes: {
368402
}
369403
}
370404
remove.forEach(v => node.removeAttribute(v));
405+
return undefined;
371406
},
372407
() => svg ? svg_element(name as keyof SVGElementTagNameMap) : element(name as keyof HTMLElementTagNameMap)
373408
);
@@ -378,7 +413,14 @@ export function claim_text(nodes: ChildNodeArray, data) {
378413
nodes,
379414
(node: ChildNode): node is Text => node.nodeType === 3,
380415
(node: Text) => {
381-
node.data = '' + data;
416+
const dataStr = '' + data;
417+
if (node.data.startsWith(dataStr)) {
418+
if (node.data.length !== dataStr.length) {
419+
return node.splitText(dataStr.length);
420+
}
421+
} else {
422+
node.data = dataStr;
423+
}
382424
},
383425
() => text(data),
384426
true // Text nodes should not update last index since it is likely not worth it to eliminate an increasing subsequence of actual elements
@@ -406,10 +448,17 @@ export function claim_html_tag(nodes) {
406448
if (start_index === end_index) {
407449
return new HtmlTag();
408450
}
451+
452+
init_claim_info(nodes);
409453
const html_tag_nodes = nodes.splice(start_index, end_index + 1);
410454
detach(html_tag_nodes[0]);
411455
detach(html_tag_nodes[html_tag_nodes.length - 1]);
412-
return new HtmlTag(html_tag_nodes.slice(1, html_tag_nodes.length - 1));
456+
const claimed_nodes = html_tag_nodes.slice(1, html_tag_nodes.length - 1);
457+
for (const n of claimed_nodes) {
458+
n.claim_order = nodes.claim_info.total_claimed;
459+
nodes.claim_info.total_claimed += 1;
460+
}
461+
return new HtmlTag(claimed_nodes);
413462
}
414463

415464
export function set_data(text, data) {
@@ -535,7 +584,7 @@ export function custom_event<T=any>(type: string, detail?: T, bubbles: boolean =
535584
}
536585

537586
export function query_selector_all(selector: string, parent: HTMLElement = document.body) {
538-
return Array.from(parent.querySelectorAll(selector));
587+
return Array.from(parent.querySelectorAll(selector)) as ChildNodeArray;
539588
}
540589

541590
export class HtmlTag {

src/runtime/internal/utils.ts

+10
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,16 @@ export function safe_not_equal(a, b) {
4040
return a != a ? b == b : a !== b || ((a && typeof a === 'object') || typeof a === 'function');
4141
}
4242

43+
let src_url_equal_anchor;
44+
45+
export function src_url_equal(element_src, url) {
46+
if (!src_url_equal_anchor) {
47+
src_url_equal_anchor = document.createElement('a');
48+
}
49+
src_url_equal_anchor.href = url;
50+
return element_src === src_url_equal_anchor.href;
51+
}
52+
4353
export function not_equal(a, b) {
4454
return a != a ? b == b : a !== b;
4555
}
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1+
<title>Some Title</title>
12
<link href="/" rel="canonical">
23
<meta content="some description" name="description">
34
<meta content="some keywords" name="keywords">
4-
<title>Some Title</title>

test/js/samples/hydrated-void-element/expected.js

+4-3
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ import {
1111
insert,
1212
noop,
1313
safe_not_equal,
14-
space
14+
space,
15+
src_url_equal
1516
} from "svelte/internal";
1617

1718
function create_fragment(ctx) {
@@ -35,7 +36,7 @@ function create_fragment(ctx) {
3536
this.h();
3637
},
3738
h() {
38-
if (img.src !== (img_src_value = "donuts.jpg")) attr(img, "src", img_src_value);
39+
if (!src_url_equal(img.src, img_src_value = "donuts.jpg")) attr(img, "src", img_src_value);
3940
attr(img, "alt", "donuts");
4041
},
4142
m(target, anchor) {
@@ -61,4 +62,4 @@ class Component extends SvelteComponent {
6162
}
6263
}
6364

64-
export default Component;
65+
export default Component;

test/js/samples/src-attribute-check/expected.js

+7-6
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import {
1010
insert,
1111
noop,
1212
safe_not_equal,
13-
space
13+
space,
14+
src_url_equal
1415
} from "svelte/internal";
1516

1617
function create_fragment(ctx) {
@@ -35,21 +36,21 @@ function create_fragment(ctx) {
3536
},
3637
h() {
3738
attr(img0, "alt", "potato");
38-
if (img0.src !== (img0_src_value = /*url*/ ctx[0])) attr(img0, "src", img0_src_value);
39+
if (!src_url_equal(img0.src, img0_src_value = /*url*/ ctx[0])) attr(img0, "src", img0_src_value);
3940
attr(img1, "alt", "potato");
40-
if (img1.src !== (img1_src_value = "" + (/*slug*/ ctx[1] + ".jpg"))) attr(img1, "src", img1_src_value);
41+
if (!src_url_equal(img1.src, img1_src_value = "" + (/*slug*/ ctx[1] + ".jpg"))) attr(img1, "src", img1_src_value);
4142
},
4243
m(target, anchor) {
4344
insert(target, img0, anchor);
4445
insert(target, t, anchor);
4546
insert(target, img1, anchor);
4647
},
4748
p(ctx, [dirty]) {
48-
if (dirty & /*url*/ 1 && img0.src !== (img0_src_value = /*url*/ ctx[0])) {
49+
if (dirty & /*url*/ 1 && !src_url_equal(img0.src, img0_src_value = /*url*/ ctx[0])) {
4950
attr(img0, "src", img0_src_value);
5051
}
5152

52-
if (dirty & /*slug*/ 2 && img1.src !== (img1_src_value = "" + (/*slug*/ ctx[1] + ".jpg"))) {
53+
if (dirty & /*slug*/ 2 && !src_url_equal(img1.src, img1_src_value = "" + (/*slug*/ ctx[1] + ".jpg"))) {
5354
attr(img1, "src", img1_src_value);
5455
}
5556
},
@@ -82,4 +83,4 @@ class Component extends SvelteComponent {
8283
}
8384
}
8485

85-
export default Component;
86+
export default Component;

0 commit comments

Comments
 (0)