Implement new hydration optimization

During hydration, greedily pick nodes that exist in the original HTML that should not be detached.
Detach the rest.
pull/6395/head
Altan Birler 5 years ago
parent 17c5402e31
commit 8e1183bfc3

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

@ -1,11 +1,38 @@
import { has_prop } from './utils';
export function append(target: Node, node: Node) {
// 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;
export function start_hydrating() {
is_hydrating = true;
}
export function end_hydrating() {
is_hydrating = false;
}
export function append(target: Node & {actual_end_child?: Node | null}, node: Node) {
if (is_hydrating) {
// If we are just starting with this target, we will insert before the firstChild (which may be null)
if (target.actual_end_child === undefined) {
target.actual_end_child = target.firstChild;
}
if (node.parentNode !== target) {
target.insertBefore(node, target.actual_end_child);
} else {
target.actual_end_child = node.nextSibling;
}
} else if (node.parentNode !== target) {
target.appendChild(node);
}
}
export function insert(target: Node, node: Node, anchor?: Node) {
if (is_hydrating && !anchor) {
append(target, node);
} else if (node.parentNode !== target || (anchor && node.nextSibling !== anchor)) {
target.insertBefore(node, anchor || null);
}
}
export function detach(node: Node) {
@ -149,42 +176,80 @@ export function time_ranges_to_array(ranges) {
return array;
}
export function children(element) {
export function children(element: HTMLElement) {
return Array.from(element.childNodes);
}
export function claim_element(nodes, name, attributes, svg) {
for (let i = 0; i < nodes.length; i += 1) {
const node = nodes[i];
if (node.nodeName === name) {
let j = 0;
const remove = [];
while (j < node.attributes.length) {
const attribute = node.attributes[j++];
if (!attributes[attribute.name]) {
remove.push(attribute.name);
type ChildNodeArray = ChildNode[] & {
/**
* All nodes at or after this index are available for preservation (not getting detached)
*/
lastKeepIndex?: number;
};
function claim_node<R extends ChildNode>(nodes: ChildNodeArray, predicate: (node: ChildNode) => node is R, processNode: (node: ChildNode) => void, createNode: () => R) {
if (nodes.lastKeepIndex === undefined) {
nodes.lastKeepIndex = 0;
}
// We first try to find a node we can actually keep without detaching
// This node should be after the previous node that we chose to keep without detaching
for (let i = nodes.lastKeepIndex; i < nodes.length; i++) {
const node = nodes[i];
if (predicate(node)) {
processNode(node);
nodes.splice(i, 1);
nodes.lastKeepIndex = i;
return node;
}
for (let k = 0; k < remove.length; k++) {
node.removeAttribute(remove[k]);
}
return nodes.splice(i, 1)[0];
// Otherwise, we try to find a node that we should detach
for (let i = 0; i < nodes.lastKeepIndex; i++) {
const node = nodes[i];
if (predicate(node)) {
processNode(node);
nodes.splice(i, 1);
nodes.lastKeepIndex -= 1;
detach(node);
return node;
}
}
return svg ? svg_element(name) : element(name);
// If we can't find any matching node, we create a new one
return createNode();
}
export function claim_text(nodes, data) {
for (let i = 0; i < nodes.length; i += 1) {
const node = nodes[i];
if (node.nodeType === 3) {
node.data = '' + data;
return nodes.splice(i, 1)[0];
export function claim_element(nodes: ChildNodeArray, name: string, attributes: {[key: string]: boolean}, svg) {
return claim_node<Element | SVGElement>(
nodes,
(node: ChildNode): node is Element | SVGElement => node.nodeName === name,
(node: Element) => {
const remove = [];
for (let j = 0; j < node.attributes.length; j++) {
const attribute = node.attributes[j];
if (!attributes[attribute.name]) {
remove.push(attribute.name);
}
}
remove.forEach(v => node.removeAttribute(v));
},
() => svg ? svg_element(name as keyof SVGElementTagNameMap) : element(name as keyof HTMLElementTagNameMap)
);
}
return text(data);
export function claim_text(nodes: ChildNodeArray, data) {
return claim_node<Text>(
nodes,
(node: ChildNode): node is Text => node.nodeType === 3,
(node: Text) => node.data = '' + data,
() => text(data)
);
}
export function claim_space(nodes) {

Loading…
Cancel
Save