Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[fix] hydration improvements #6449

Merged
merged 12 commits into from
Jul 21, 2021
4 changes: 2 additions & 2 deletions src/compiler/compile/render_dom/wrappers/Element/Attribute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ export default class AttributeWrapper extends BaseAttributeWrapper {
`);
} else if (this.is_src) {
block.chunks.hydrate.push(
b`if (${element.var}.src !== ${init}) ${method}(${element.var}, "${name}", ${this.last});`
b`if (!@src_url_equal(${element.var}.src, ${init})) ${method}(${element.var}, "${name}", ${this.last});`
);
updater = b`${method}(${element.var}, "${name}", ${should_cache ? this.last : value});`;
} else if (property_name) {
Expand Down Expand Up @@ -193,7 +193,7 @@ export default class AttributeWrapper extends BaseAttributeWrapper {

if (should_cache) {
condition = this.is_src
? x`${condition} && (${element.var}.src !== (${last} = ${value}))`
? x`${condition} && (!@src_url_equal(${element.var}.src, (${last} = ${value})))`
: x`${condition} && (${last} !== (${last} = ${value}))`;
}

Expand Down
79 changes: 64 additions & 15 deletions src/runtime/internal/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export function end_hydrating() {
type NodeEx = Node & {
claim_order?: number,
hydrate_init? : true,
actual_end_child?: Node,
actual_end_child?: NodeEx,
childNodes: NodeListOf<NodeEx>,
};

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

type NodeEx2 = NodeEx & {claim_order: number};

// We know that all children have claim_order values since the unclaimed have been detached
const children = target.childNodes as NodeListOf<NodeEx2>;
// We know that all children have claim_order values since the unclaimed have been detached if target is not <head>
let children: ArrayLike<NodeEx2> = target.childNodes as NodeListOf<NodeEx2>;

// If target is <head>, there may be children without claim_order
if (target.nodeName === 'HEAD') {
const myChildren = [];
for (let i = 0; i < children.length; i++) {
const node = children[i];
if (node.claim_order !== undefined) {
myChildren.push(node);
}
}
children = myChildren;
}

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

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

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

Expand Down Expand Up @@ -119,8 +132,17 @@ export function append(target: NodeEx, node: NodeEx) {
if ((target.actual_end_child === undefined) || ((target.actual_end_child !== null) && (target.actual_end_child.parentElement !== target))) {
target.actual_end_child = target.firstChild;
}

// Skip nodes of undefined ordering
while ((target.actual_end_child !== null) && (target.actual_end_child.claim_order === undefined)) {
target.actual_end_child = target.actual_end_child.nextSibling;
}

if (node !== target.actual_end_child) {
target.insertBefore(node, target.actual_end_child);
// We only insert if the ordering of this node should be modified or the parent node is not target
if (node.claim_order !== undefined || node.parentNode !== target) {
target.insertBefore(node, target.actual_end_child);
}
} else {
target.actual_end_child = node.nextSibling;
}
Expand Down Expand Up @@ -304,21 +326,29 @@ export function children(element: Element) {
return Array.from(element.childNodes);
}

function claim_node<R extends ChildNodeEx>(nodes: ChildNodeArray, predicate: (node: ChildNodeEx) => node is R, processNode: (node: ChildNodeEx) => void, createNode: () => R, dontUpdateLastIndex: boolean = false) {
// Try to find nodes in an order such that we lengthen the longest increasing subsequence
function init_claim_info(nodes: ChildNodeArray) {
if (nodes.claim_info === undefined) {
nodes.claim_info = {last_index: 0, total_claimed: 0};
}
}

function claim_node<R extends ChildNodeEx>(nodes: ChildNodeArray, predicate: (node: ChildNodeEx) => node is R, processNode: (node: ChildNodeEx) => ChildNodeEx | undefined, createNode: () => R, dontUpdateLastIndex: boolean = false) {
// Try to find nodes in an order such that we lengthen the longest increasing subsequence
init_claim_info(nodes);

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

if (predicate(node)) {
processNode(node);
const replacement = processNode(node);

nodes.splice(i, 1);
if (replacement === undefined) {
nodes.splice(i, 1);
} else {
nodes[i] = replacement;
}
if (!dontUpdateLastIndex) {
nodes.claim_info.last_index = i;
}
Expand All @@ -333,12 +363,16 @@ function claim_node<R extends ChildNodeEx>(nodes: ChildNodeArray, predicate: (no
const node = nodes[i];

if (predicate(node)) {
processNode(node);
const replacement = processNode(node);

nodes.splice(i, 1);
if (replacement === undefined) {
nodes.splice(i, 1);
} else {
nodes[i] = replacement;
}
if (!dontUpdateLastIndex) {
nodes.claim_info.last_index = i;
} else {
} else if (replacement === undefined) {
// Since we spliced before the last_index, we decrease it
nodes.claim_info.last_index--;
}
Expand Down Expand Up @@ -368,6 +402,7 @@ export function claim_element(nodes: ChildNodeArray, name: string, attributes: {
}
}
remove.forEach(v => node.removeAttribute(v));
return undefined;
},
() => svg ? svg_element(name as keyof SVGElementTagNameMap) : element(name as keyof HTMLElementTagNameMap)
);
Expand All @@ -378,7 +413,14 @@ export function claim_text(nodes: ChildNodeArray, data) {
nodes,
(node: ChildNode): node is Text => node.nodeType === 3,
(node: Text) => {
node.data = '' + data;
const dataStr = '' + data;
if (node.data.startsWith(dataStr)) {
if (node.data.length !== dataStr.length) {
return node.splitText(dataStr.length);
}
} else {
node.data = dataStr;
}
},
() => text(data),
true // Text nodes should not update last index since it is likely not worth it to eliminate an increasing subsequence of actual elements
Expand Down Expand Up @@ -406,10 +448,17 @@ export function claim_html_tag(nodes) {
if (start_index === end_index) {
return new HtmlTag();
}

init_claim_info(nodes);
const html_tag_nodes = nodes.splice(start_index, end_index + 1);
detach(html_tag_nodes[0]);
detach(html_tag_nodes[html_tag_nodes.length - 1]);
return new HtmlTag(html_tag_nodes.slice(1, html_tag_nodes.length - 1));
const claimed_nodes = html_tag_nodes.slice(1, html_tag_nodes.length - 1);
for (const n of claimed_nodes) {
n.claim_order = nodes.claim_info.total_claimed;
nodes.claim_info.total_claimed += 1;
}
return new HtmlTag(claimed_nodes);
}

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

export function query_selector_all(selector: string, parent: HTMLElement = document.body) {
return Array.from(parent.querySelectorAll(selector));
return Array.from(parent.querySelectorAll(selector)) as ChildNodeArray;
}

export class HtmlTag {
Expand Down
10 changes: 10 additions & 0 deletions src/runtime/internal/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,16 @@ export function safe_not_equal(a, b) {
return a != a ? b == b : a !== b || ((a && typeof a === 'object') || typeof a === 'function');
}

let src_url_equal_anchor;

export function src_url_equal(element_src, url) {
if (!src_url_equal_anchor) {
src_url_equal_anchor = document.createElement('a');
}
src_url_equal_anchor.href = url;
return element_src === src_url_equal_anchor.href;
}

export function not_equal(a, b) {
return a != a ? b == b : a !== b;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<title>Some Title</title>
<link href="/" rel="canonical">
<meta content="some description" name="description">
<meta content="some keywords" name="keywords">
<title>Some Title</title>
7 changes: 4 additions & 3 deletions test/js/samples/hydrated-void-element/expected.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import {
insert,
noop,
safe_not_equal,
space
space,
src_url_equal
} from "svelte/internal";

function create_fragment(ctx) {
Expand All @@ -35,7 +36,7 @@ function create_fragment(ctx) {
this.h();
},
h() {
if (img.src !== (img_src_value = "donuts.jpg")) attr(img, "src", img_src_value);
if (!src_url_equal(img.src, img_src_value = "donuts.jpg")) attr(img, "src", img_src_value);
attr(img, "alt", "donuts");
},
m(target, anchor) {
Expand All @@ -61,4 +62,4 @@ class Component extends SvelteComponent {
}
}

export default Component;
export default Component;
13 changes: 7 additions & 6 deletions test/js/samples/src-attribute-check/expected.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import {
insert,
noop,
safe_not_equal,
space
space,
src_url_equal
} from "svelte/internal";

function create_fragment(ctx) {
Expand All @@ -35,21 +36,21 @@ function create_fragment(ctx) {
},
h() {
attr(img0, "alt", "potato");
if (img0.src !== (img0_src_value = /*url*/ ctx[0])) attr(img0, "src", img0_src_value);
if (!src_url_equal(img0.src, img0_src_value = /*url*/ ctx[0])) attr(img0, "src", img0_src_value);
attr(img1, "alt", "potato");
if (img1.src !== (img1_src_value = "" + (/*slug*/ ctx[1] + ".jpg"))) attr(img1, "src", img1_src_value);
if (!src_url_equal(img1.src, img1_src_value = "" + (/*slug*/ ctx[1] + ".jpg"))) attr(img1, "src", img1_src_value);
},
m(target, anchor) {
insert(target, img0, anchor);
insert(target, t, anchor);
insert(target, img1, anchor);
},
p(ctx, [dirty]) {
if (dirty & /*url*/ 1 && img0.src !== (img0_src_value = /*url*/ ctx[0])) {
if (dirty & /*url*/ 1 && !src_url_equal(img0.src, img0_src_value = /*url*/ ctx[0])) {
attr(img0, "src", img0_src_value);
}

if (dirty & /*slug*/ 2 && img1.src !== (img1_src_value = "" + (/*slug*/ ctx[1] + ".jpg"))) {
if (dirty & /*slug*/ 2 && !src_url_equal(img1.src, img1_src_value = "" + (/*slug*/ ctx[1] + ".jpg"))) {
attr(img1, "src", img1_src_value);
}
},
Expand Down Expand Up @@ -82,4 +83,4 @@ class Component extends SvelteComponent {
}
}

export default Component;
export default Component;