From ecbd96af95eddf7d53d24fa78aeb5818f2cf71b9 Mon Sep 17 00:00:00 2001 From: Hasan Altan Birler Date: Wed, 21 Jul 2021 18:28:44 +0200 Subject: [PATCH] [fix] hydration improvements (#6449) --- .../render_dom/wrappers/Element/Attribute.ts | 4 +- src/runtime/internal/dom.ts | 79 +++++++++++++++---- src/runtime/internal/utils.ts | 10 +++ .../_after_head.html | 2 +- .../samples/hydrated-void-element/expected.js | 7 +- .../samples/src-attribute-check/expected.js | 13 +-- 6 files changed, 88 insertions(+), 27 deletions(-) diff --git a/src/compiler/compile/render_dom/wrappers/Element/Attribute.ts b/src/compiler/compile/render_dom/wrappers/Element/Attribute.ts index b9dbd0952d03..179635eb5789 100644 --- a/src/compiler/compile/render_dom/wrappers/Element/Attribute.ts +++ b/src/compiler/compile/render_dom/wrappers/Element/Attribute.ts @@ -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) { @@ -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}))`; } diff --git a/src/runtime/internal/dom.ts b/src/runtime/internal/dom.ts index a055bee4460a..c1909dafe213 100644 --- a/src/runtime/internal/dom.ts +++ b/src/runtime/internal/dom.ts @@ -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, }; @@ -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; + // We know that all children have claim_order values since the unclaimed have been detached if target is not + let children: ArrayLike = target.childNodes as NodeListOf; + + // If target is , 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. @@ -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; @@ -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; } @@ -304,11 +326,15 @@ export function children(element: Element) { return Array.from(element.childNodes); } -function claim_node(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(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 @@ -316,9 +342,13 @@ function claim_node(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; } @@ -333,12 +363,16 @@ function claim_node(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--; } @@ -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) ); @@ -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 @@ -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) { @@ -535,7 +584,7 @@ export function custom_event(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 { diff --git a/src/runtime/internal/utils.ts b/src/runtime/internal/utils.ts index 9f3da8589a9c..9a372617239c 100644 --- a/src/runtime/internal/utils.ts +++ b/src/runtime/internal/utils.ts @@ -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; } diff --git a/test/hydration/samples/head-meta-hydrate-duplicate/_after_head.html b/test/hydration/samples/head-meta-hydrate-duplicate/_after_head.html index 9016a44869ec..be7a01ba4ff7 100644 --- a/test/hydration/samples/head-meta-hydrate-duplicate/_after_head.html +++ b/test/hydration/samples/head-meta-hydrate-duplicate/_after_head.html @@ -1,4 +1,4 @@ +Some Title -Some Title diff --git a/test/js/samples/hydrated-void-element/expected.js b/test/js/samples/hydrated-void-element/expected.js index e53d16d9250e..2df469e68903 100644 --- a/test/js/samples/hydrated-void-element/expected.js +++ b/test/js/samples/hydrated-void-element/expected.js @@ -11,7 +11,8 @@ import { insert, noop, safe_not_equal, - space + space, + src_url_equal } from "svelte/internal"; function create_fragment(ctx) { @@ -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) { @@ -61,4 +62,4 @@ class Component extends SvelteComponent { } } -export default Component; \ No newline at end of file +export default Component; diff --git a/test/js/samples/src-attribute-check/expected.js b/test/js/samples/src-attribute-check/expected.js index c68187c4954a..8a13cff21c65 100644 --- a/test/js/samples/src-attribute-check/expected.js +++ b/test/js/samples/src-attribute-check/expected.js @@ -10,7 +10,8 @@ import { insert, noop, safe_not_equal, - space + space, + src_url_equal } from "svelte/internal"; function create_fragment(ctx) { @@ -35,9 +36,9 @@ 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); @@ -45,11 +46,11 @@ function create_fragment(ctx) { 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); } }, @@ -82,4 +83,4 @@ class Component extends SvelteComponent { } } -export default Component; \ No newline at end of file +export default Component;