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

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

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

@ -12,6 +12,7 @@ import { create_fragment_from_html } from '../reconciler.js';
import { assign_nodes } from '../template.js'; import { assign_nodes } from '../template.js';
import * as w from '../../warnings.js'; import * as w from '../../warnings.js';
import { DEV } from 'esm-env'; import { DEV } from 'esm-env';
import { get_first_child, get_next_sibling } from '../operations.js';
/** /**
* @template {(node: TemplateNode, ...args: any[]) => void} SnippetFn * @template {(node: TemplateNode, ...args: any[]) => void} SnippetFn
@ -89,9 +90,9 @@ export function createRawSnippet(fn) {
} else { } else {
var html = snippet.render().trim(); var html = snippet.render().trim();
var fragment = create_fragment_from_html(html); 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(); w.invalid_raw_snippet_render();
} }

@ -7,7 +7,7 @@ import {
set_hydrate_node, set_hydrate_node,
set_hydrating set_hydrating
} from '../hydration.js'; } from '../hydration.js';
import { create_text } from '../operations.js'; import { create_text, get_first_child } from '../operations.js';
import { import {
block, block,
branch, 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 // 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 // inner open and close methods can pick up the existing nodes correctly
var child_anchor = /** @type {TemplateNode} */ ( var child_anchor = /** @type {TemplateNode} */ (
hydrating ? element.firstChild : element.appendChild(create_text()) hydrating ? get_first_child(element) : element.appendChild(create_text())
); );
if (hydrating) { if (hydrating) {

@ -1,6 +1,6 @@
/** @import { TemplateNode } from '#client' */ /** @import { TemplateNode } from '#client' */
import { hydrate_node, hydrating, set_hydrate_node, set_hydrating } from '../hydration.js'; 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 { block } from '../../reactivity/effects.js';
import { HEAD_EFFECT } from '../../constants.js'; import { HEAD_EFFECT } from '../../constants.js';
import { HYDRATION_START } 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. // There might be multiple head blocks in our app, so we need to account for each one needing independent hydration.
if (head_anchor === undefined) { if (head_anchor === undefined) {
head_anchor = /** @type {TemplateNode} */ (document.head.firstChild); head_anchor = /** @type {TemplateNode} */ (get_first_child(document.head));
} }
while ( while (
head_anchor !== null && head_anchor !== null &&
(head_anchor.nodeType !== 8 || /** @type {Comment} */ (head_anchor).data !== HYDRATION_START) (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 // 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) { if (head_anchor === null) {
set_hydrating(false); set_hydrating(false);
} else { } 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 { 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'; import { queue_micro_task } from '../task.js';
/** /**
@ -27,7 +27,7 @@ export function autofocus(dom, value) {
* @returns {void} * @returns {void}
*/ */
export function remove_textarea_child(dom) { export function remove_textarea_child(dom) {
if (hydrating && dom.firstChild !== null) { if (hydrating && get_first_child(dom) !== null) {
clear_text_content(dom); clear_text_content(dom);
} }
} }

@ -7,6 +7,7 @@ import {
HYDRATION_START_ELSE HYDRATION_START_ELSE
} from '../../../constants.js'; } from '../../../constants.js';
import * as w from '../warnings.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 * 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() { 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 */ /** @param {TemplateNode} node */
@ -47,7 +48,7 @@ export function reset(node) {
if (!hydrating) return; if (!hydrating) return;
// If the node has remaining siblings, something has gone wrong // 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(); w.hydration_mismatch();
throw HYDRATION_ERROR; 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.remove();
node = next; node = next;
} }

@ -2,6 +2,7 @@
import { hydrate_node, hydrating, set_hydrate_node } from './hydration.js'; import { hydrate_node, hydrating, set_hydrate_node } from './hydration.js';
import { DEV } from 'esm-env'; import { DEV } from 'esm-env';
import { init_array_prototype_warnings } from '../dev/equality.js'; 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 // export these for reference in the compiled code, making global name deduplication unnecessary
/** @type {Window} */ /** @type {Window} */
@ -10,6 +11,11 @@ export var $window;
/** @type {Document} */ /** @type {Document} */
export var $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 * 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 * where these globals are not available while avoiding a separate server entry point
@ -23,6 +29,12 @@ export function init_operations() {
$document = document; $document = document;
var element_prototype = Element.prototype; 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 // the following assignments improve perf of lookups on DOM nodes
// @ts-expect-error // @ts-expect-error
@ -53,6 +65,24 @@ export function create_text(value = '') {
return document.createTextNode(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 * Don't mark this as side-effect-free, hydration needs to walk all nodes
* @template {Node} N * @template {Node} N
@ -61,10 +91,10 @@ export function create_text(value = '') {
*/ */
export function child(node) { export function child(node) {
if (!hydrating) { 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 // Child can be null if we have an element with a single child, like `<p>{text}</p>`, where `text` is empty
if (child === null) { if (child === null) {
@ -84,10 +114,10 @@ export function child(node) {
export function first_child(fragment, is_text) { export function first_child(fragment, is_text) {
if (!hydrating) { if (!hydrating) {
// when not hydrating, `fragment` is a `DocumentFragment` (the result of calling `open_frag`) // 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 // 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; return first;
} }
@ -114,10 +144,10 @@ export function first_child(fragment, is_text) {
*/ */
export function sibling(node, is_text = false) { export function sibling(node, is_text = false) {
if (!hydrating) { 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; var type = next_sibling.nodeType;

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

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

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

Loading…
Cancel
Save