diff --git a/.changeset/odd-toys-glow.md b/.changeset/odd-toys-glow.md new file mode 100644 index 0000000000..cc09a78ea2 --- /dev/null +++ b/.changeset/odd-toys-glow.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: improve performance of DOM traversal operations diff --git a/packages/svelte/src/internal/client/dom/blocks/css-props.js b/packages/svelte/src/internal/client/dom/blocks/css-props.js index c5d63fc9a6..473d35b122 100644 --- a/packages/svelte/src/internal/client/dom/blocks/css-props.js +++ b/packages/svelte/src/internal/client/dom/blocks/css-props.js @@ -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(() => { diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index e820e15e46..18785f7114 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -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; } diff --git a/packages/svelte/src/internal/client/dom/blocks/html.js b/packages/svelte/src/internal/client/dom/blocks/html.js index 63ca704c0f..cdfa1baa71 100644 --- a/packages/svelte/src/internal/client/dom/blocks/html.js +++ b/packages/svelte/src/internal/client/dom/blocks/html.js @@ -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); diff --git a/packages/svelte/src/internal/client/dom/blocks/snippet.js b/packages/svelte/src/internal/client/dom/blocks/snippet.js index 6a6203d16e..32a9fb6918 100644 --- a/packages/svelte/src/internal/client/dom/blocks/snippet.js +++ b/packages/svelte/src/internal/client/dom/blocks/snippet.js @@ -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(); } diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-element.js b/packages/svelte/src/internal/client/dom/blocks/svelte-element.js index fff79f5bc9..755e44c9ac 100644 --- a/packages/svelte/src/internal/client/dom/blocks/svelte-element.js +++ b/packages/svelte/src/internal/client/dom/blocks/svelte-element.js @@ -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) { diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-head.js b/packages/svelte/src/internal/client/dom/blocks/svelte-head.js index e1a7fb62c1..e3e3eacad7 100644 --- a/packages/svelte/src/internal/client/dom/blocks/svelte-head.js +++ b/packages/svelte/src/internal/client/dom/blocks/svelte-head.js @@ -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))); } } diff --git a/packages/svelte/src/internal/client/dom/elements/misc.js b/packages/svelte/src/internal/client/dom/elements/misc.js index b2007689f9..61e513903f 100644 --- a/packages/svelte/src/internal/client/dom/elements/misc.js +++ b/packages/svelte/src/internal/client/dom/elements/misc.js @@ -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); } } diff --git a/packages/svelte/src/internal/client/dom/hydration.js b/packages/svelte/src/internal/client/dom/hydration.js index 82ad0df613..62b40c8ccd 100644 --- a/packages/svelte/src/internal/client/dom/hydration.js +++ b/packages/svelte/src/internal/client/dom/hydration.js @@ -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; } diff --git a/packages/svelte/src/internal/client/dom/operations.js b/packages/svelte/src/internal/client/dom/operations.js index 207079cad5..b86ea77947 100644 --- a/packages/svelte/src/internal/client/dom/operations.js +++ b/packages/svelte/src/internal/client/dom/operations.js @@ -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 `

{text}

`, 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; diff --git a/packages/svelte/src/internal/client/dom/template.js b/packages/svelte/src/internal/client/dom/template.js index 1405aec7cf..27d82bb0a4 100644 --- a/packages/svelte/src/internal/client/dom/template.js +++ b/packages/svelte/src/internal/client/dom/template.js @@ -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); diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index c5be02466e..68a309cbda 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -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; diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index f70e055ee5..044c09e310 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -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) {