chore: improve performance of DOM traversal operations (#12863)

* chore: improve performance of DOM traversal operations

* feedback

---------

Co-authored-by: Rich Harris <rich.harris@vercel.com>
pull/12870/head
Dominic Gannaway 4 months ago committed by GitHub
parent d421838272
commit 72c51e3df7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
chore: improve performance of DOM traversal operations

@ -1,6 +1,7 @@
/** @import { TemplateNode } from '#client' */
import { render_effect, teardown } from '../../reactivity/effects.js';
import { hydrate_node, hydrating, set_hydrate_node } from '../hydration.js';
import { get_first_child } from '../operations.js';
/**
* @param {HTMLDivElement | SVGGElement} element
@ -9,7 +10,7 @@ import { hydrate_node, hydrating, set_hydrate_node } from '../hydration.js';
*/
export function css_props(element, get_styles) {
if (hydrating) {
set_hydrate_node(/** @type {TemplateNode} */ (element.firstChild));
set_hydrate_node(/** @type {TemplateNode} */ (get_first_child(element)));
}
render_effect(() => {

@ -16,7 +16,12 @@ import {
set_hydrate_node,
set_hydrating
} from '../hydration.js';
import { clear_text_content, create_text } from '../operations.js';
import {
clear_text_content,
create_text,
get_first_child,
get_next_sibling
} from '../operations.js';
import {
block,
branch,
@ -116,7 +121,7 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
var parent_node = /** @type {Element} */ (node);
anchor = hydrating
? set_hydrate_node(/** @type {Comment | Text} */ (parent_node.firstChild))
? set_hydrate_node(/** @type {Comment | Text} */ (get_first_child(parent_node)))
: parent_node.appendChild(create_text());
}
@ -510,7 +515,7 @@ function move(item, next, anchor) {
var node = /** @type {EffectNodes} */ (item.e.nodes).start;
while (node !== end) {
var next_node = /** @type {TemplateNode} */ (node.nextSibling);
var next_node = /** @type {TemplateNode} */ (get_next_sibling(node));
dest.before(node);
node = next_node;
}

@ -8,6 +8,7 @@ import * as w from '../../warnings.js';
import { hash } from '../../../../utils.js';
import { DEV } from 'esm-env';
import { dev_current_component_function } from '../../runtime.js';
import { get_first_child, get_next_sibling } from '../operations.js';
/**
* @param {Element} element
@ -71,7 +72,7 @@ export function html(node, get_value, svg, mathml, skip_warning) {
(next.nodeType !== 8 || /** @type {Comment} */ (next).data !== '')
) {
last = next;
next = /** @type {TemplateNode} */ (next.nextSibling);
next = /** @type {TemplateNode} */ (get_next_sibling(next));
}
if (next === null) {
@ -98,17 +99,17 @@ export function html(node, get_value, svg, mathml, skip_warning) {
var node = create_fragment_from_html(html);
if (svg || mathml) {
node = /** @type {Element} */ (node.firstChild);
node = /** @type {Element} */ (get_first_child(node));
}
assign_nodes(
/** @type {TemplateNode} */ (node.firstChild),
/** @type {TemplateNode} */ (get_first_child(node)),
/** @type {TemplateNode} */ (node.lastChild)
);
if (svg || mathml) {
while (node.firstChild) {
anchor.before(node.firstChild);
while (get_first_child(node)) {
anchor.before(/** @type {Node} */ (get_first_child(node)));
}
} else {
anchor.before(node);

@ -12,6 +12,7 @@ import { create_fragment_from_html } from '../reconciler.js';
import { assign_nodes } from '../template.js';
import * as w from '../../warnings.js';
import { DEV } from 'esm-env';
import { get_first_child, get_next_sibling } from '../operations.js';
/**
* @template {(node: TemplateNode, ...args: any[]) => void} SnippetFn
@ -89,9 +90,9 @@ export function createRawSnippet(fn) {
} else {
var html = snippet.render().trim();
var fragment = create_fragment_from_html(html);
element = /** @type {Element} */ (fragment.firstChild);
element = /** @type {Element} */ (get_first_child(fragment));
if (DEV && (element.nextSibling !== null || element.nodeType !== 3)) {
if (DEV && (get_next_sibling(element) !== null || element.nodeType !== 3)) {
w.invalid_raw_snippet_render();
}

@ -7,7 +7,7 @@ import {
set_hydrate_node,
set_hydrating
} from '../hydration.js';
import { create_text } from '../operations.js';
import { create_text, get_first_child } from '../operations.js';
import {
block,
branch,
@ -119,7 +119,7 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio
// If hydrating, use the existing ssr comment as the anchor so that the
// inner open and close methods can pick up the existing nodes correctly
var child_anchor = /** @type {TemplateNode} */ (
hydrating ? element.firstChild : element.appendChild(create_text())
hydrating ? get_first_child(element) : element.appendChild(create_text())
);
if (hydrating) {

@ -1,6 +1,6 @@
/** @import { TemplateNode } from '#client' */
import { hydrate_node, hydrating, set_hydrate_node, set_hydrating } from '../hydration.js';
import { create_text } from '../operations.js';
import { create_text, get_first_child, get_next_sibling } from '../operations.js';
import { block } from '../../reactivity/effects.js';
import { HEAD_EFFECT } from '../../constants.js';
import { HYDRATION_START } from '../../../../constants.js';
@ -32,14 +32,14 @@ export function head(render_fn) {
// There might be multiple head blocks in our app, so we need to account for each one needing independent hydration.
if (head_anchor === undefined) {
head_anchor = /** @type {TemplateNode} */ (document.head.firstChild);
head_anchor = /** @type {TemplateNode} */ (get_first_child(document.head));
}
while (
head_anchor !== null &&
(head_anchor.nodeType !== 8 || /** @type {Comment} */ (head_anchor).data !== HYDRATION_START)
) {
head_anchor = /** @type {TemplateNode} */ (head_anchor.nextSibling);
head_anchor = /** @type {TemplateNode} */ (get_next_sibling(head_anchor));
}
// If we can't find an opening hydration marker, skip hydration (this can happen
@ -47,7 +47,7 @@ export function head(render_fn) {
if (head_anchor === null) {
set_hydrating(false);
} else {
head_anchor = set_hydrate_node(/** @type {TemplateNode} */ (head_anchor.nextSibling));
head_anchor = set_hydrate_node(/** @type {TemplateNode} */ (get_next_sibling(head_anchor)));
}
}

@ -1,5 +1,5 @@
import { hydrating } from '../hydration.js';
import { clear_text_content } from '../operations.js';
import { clear_text_content, get_first_child } from '../operations.js';
import { queue_micro_task } from '../task.js';
/**
@ -27,7 +27,7 @@ export function autofocus(dom, value) {
* @returns {void}
*/
export function remove_textarea_child(dom) {
if (hydrating && dom.firstChild !== null) {
if (hydrating && get_first_child(dom) !== null) {
clear_text_content(dom);
}
}

@ -7,6 +7,7 @@ import {
HYDRATION_START_ELSE
} from '../../../constants.js';
import * as w from '../warnings.js';
import { get_next_sibling } from './operations.js';
/**
* Use this variable to guard everything related to hydration code so it can be treeshaken out
@ -39,7 +40,7 @@ export function set_hydrate_node(node) {
}
export function hydrate_next() {
return set_hydrate_node(/** @type {TemplateNode} */ (hydrate_node.nextSibling));
return set_hydrate_node(/** @type {TemplateNode} */ (get_next_sibling(hydrate_node)));
}
/** @param {TemplateNode} node */
@ -47,7 +48,7 @@ export function reset(node) {
if (!hydrating) return;
// If the node has remaining siblings, something has gone wrong
if (hydrate_node.nextSibling !== null) {
if (get_next_sibling(hydrate_node) !== null) {
w.hydration_mismatch();
throw HYDRATION_ERROR;
}
@ -90,7 +91,7 @@ export function remove_nodes() {
}
}
var next = /** @type {TemplateNode} */ (node.nextSibling);
var next = /** @type {TemplateNode} */ (get_next_sibling(node));
node.remove();
node = next;
}

@ -2,6 +2,7 @@
import { hydrate_node, hydrating, set_hydrate_node } from './hydration.js';
import { DEV } from 'esm-env';
import { init_array_prototype_warnings } from '../dev/equality.js';
import { get_descriptor } from '../../shared/utils.js';
// export these for reference in the compiled code, making global name deduplication unnecessary
/** @type {Window} */
@ -10,6 +11,11 @@ export var $window;
/** @type {Document} */
export var $document;
/** @type {() => Node | null} */
var first_child_getter;
/** @type {() => Node | null} */
var next_sibling_getter;
/**
* Initialize these lazily to avoid issues when using the runtime in a server context
* where these globals are not available while avoiding a separate server entry point
@ -23,6 +29,12 @@ export function init_operations() {
$document = document;
var element_prototype = Element.prototype;
var node_prototype = Node.prototype;
// @ts-ignore
first_child_getter = get_descriptor(node_prototype, 'firstChild').get;
// @ts-ignore
next_sibling_getter = get_descriptor(node_prototype, 'nextSibling').get;
// the following assignments improve perf of lookups on DOM nodes
// @ts-expect-error
@ -53,6 +65,24 @@ export function create_text(value = '') {
return document.createTextNode(value);
}
/**
* @template {Node} N
* @param {N} node
* @returns {Node | null}
*/
export function get_first_child(node) {
return first_child_getter.call(node);
}
/**
* @template {Node} N
* @param {N} node
* @returns {Node | null}
*/
export function get_next_sibling(node) {
return next_sibling_getter.call(node);
}
/**
* Don't mark this as side-effect-free, hydration needs to walk all nodes
* @template {Node} N
@ -61,10 +91,10 @@ export function create_text(value = '') {
*/
export function child(node) {
if (!hydrating) {
return node.firstChild;
return get_first_child(node);
}
var child = /** @type {TemplateNode} */ (hydrate_node.firstChild);
var child = /** @type {TemplateNode} */ (get_first_child(hydrate_node));
// Child can be null if we have an element with a single child, like `<p>{text}</p>`, where `text` is empty
if (child === null) {
@ -84,10 +114,10 @@ export function child(node) {
export function first_child(fragment, is_text) {
if (!hydrating) {
// when not hydrating, `fragment` is a `DocumentFragment` (the result of calling `open_frag`)
var first = /** @type {DocumentFragment} */ (fragment).firstChild;
var first = /** @type {DocumentFragment} */ (get_first_child(/** @type {Node} */ (fragment)));
// TODO prevent user comments with the empty string when preserveComments is true
if (first instanceof Comment && first.data === '') return first.nextSibling;
if (first instanceof Comment && first.data === '') return get_next_sibling(first);
return first;
}
@ -114,10 +144,10 @@ export function first_child(fragment, is_text) {
*/
export function sibling(node, is_text = false) {
if (!hydrating) {
return /** @type {TemplateNode} */ (node.nextSibling);
return /** @type {TemplateNode} */ (get_next_sibling(node));
}
var next_sibling = /** @type {TemplateNode} */ (hydrate_node.nextSibling);
var next_sibling = /** @type {TemplateNode} */ (get_next_sibling(hydrate_node));
var type = next_sibling.nodeType;

@ -1,6 +1,6 @@
/** @import { Effect, EffectNodes, TemplateNode } from '#client' */
import { hydrate_next, hydrate_node, hydrating, set_hydrate_node } from './hydration.js';
import { create_text } from './operations.js';
import { create_text, get_first_child } from './operations.js';
import { create_fragment_from_html } from './reconciler.js';
import { current_effect } from '../runtime.js';
import { TEMPLATE_FRAGMENT, TEMPLATE_USE_IMPORT_NODE } from '../../../constants.js';
@ -41,7 +41,7 @@ export function template(content, flags) {
if (!node) {
node = create_fragment_from_html(has_start ? content : '<!>' + content);
if (!is_fragment) node = /** @type {Node} */ (node.firstChild);
if (!is_fragment) node = /** @type {Node} */ (get_first_child(node));
}
var clone = /** @type {TemplateNode} */ (
@ -49,7 +49,7 @@ export function template(content, flags) {
);
if (is_fragment) {
var start = /** @type {TemplateNode} */ (clone.firstChild);
var start = /** @type {TemplateNode} */ (get_first_child(clone));
var end = /** @type {TemplateNode} */ (clone.lastChild);
assign_nodes(start, end);
@ -113,22 +113,22 @@ export function ns_template(content, flags, ns = 'svg') {
if (!node) {
var fragment = /** @type {DocumentFragment} */ (create_fragment_from_html(wrapped));
var root = /** @type {Element} */ (fragment.firstChild);
var root = /** @type {Element} */ (get_first_child(fragment));
if (is_fragment) {
node = document.createDocumentFragment();
while (root.firstChild) {
node.appendChild(root.firstChild);
while (get_first_child(root)) {
node.appendChild(/** @type {Node} */ (get_first_child(root)));
}
} else {
node = /** @type {Element} */ (root.firstChild);
node = /** @type {Element} */ (get_first_child(root));
}
}
var clone = /** @type {TemplateNode} */ (node.cloneNode(true));
if (is_fragment) {
var start = /** @type {TemplateNode} */ (clone.firstChild);
var start = /** @type {TemplateNode} */ (get_first_child(clone));
var end = /** @type {TemplateNode} */ (clone.lastChild);
assign_nodes(start, end);

@ -39,6 +39,7 @@ import { set } from './sources.js';
import * as e from '../errors.js';
import { DEV } from 'esm-env';
import { define_property } from '../../shared/utils.js';
import { get_next_sibling } from '../dom/operations.js';
/**
* @param {'$effect' | '$effect.pre' | '$inspect'} rune
@ -361,7 +362,7 @@ export function destroy_effect(effect, remove_dom = true) {
while (node !== null) {
/** @type {TemplateNode | null} */
var next = node === end ? null : /** @type {TemplateNode} */ (node.nextSibling);
var next = node === end ? null : /** @type {TemplateNode} */ (get_next_sibling(node));
node.remove();
node = next;

@ -1,7 +1,13 @@
/** @import { ComponentContext, Effect, EffectNodes, TemplateNode } from '#client' */
/** @import { Component, ComponentType, SvelteComponent } from '../../index.js' */
import { DEV } from 'esm-env';
import { clear_text_content, create_text, init_operations } from './dom/operations.js';
import {
clear_text_content,
create_text,
get_first_child,
get_next_sibling,
init_operations
} from './dom/operations.js';
import { HYDRATION_END, HYDRATION_ERROR, HYDRATION_START } from '../../constants.js';
import { push, pop, current_component_context, current_effect } from './runtime.js';
import { effect_root, branch } from './reactivity/effects.js';
@ -102,18 +108,19 @@ export function mount(component, options) {
* @returns {Exports}
*/
export function hydrate(component, options) {
init_operations();
options.intro = options.intro ?? false;
const target = options.target;
const was_hydrating = hydrating;
const previous_hydrate_node = hydrate_node;
try {
var anchor = /** @type {TemplateNode} */ (target.firstChild);
var anchor = /** @type {TemplateNode} */ (get_first_child(target));
while (
anchor &&
(anchor.nodeType !== 8 || /** @type {Comment} */ (anchor).data !== HYDRATION_START)
) {
anchor = /** @type {TemplateNode} */ (anchor.nextSibling);
anchor = /** @type {TemplateNode} */ (get_next_sibling(anchor));
}
if (!anchor) {

Loading…
Cancel
Save