Skip to content

Commit 72c51e3

Browse files
trueadmRich-Harris
andauthored
chore: improve performance of DOM traversal operations (#12863)
* chore: improve performance of DOM traversal operations * feedback --------- Co-authored-by: Rich Harris <[email protected]>
1 parent d421838 commit 72c51e3

File tree

13 files changed

+92
-40
lines changed

13 files changed

+92
-40
lines changed

.changeset/odd-toys-glow.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
chore: improve performance of DOM traversal operations

packages/svelte/src/internal/client/dom/blocks/css-props.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/** @import { TemplateNode } from '#client' */
22
import { render_effect, teardown } from '../../reactivity/effects.js';
33
import { hydrate_node, hydrating, set_hydrate_node } from '../hydration.js';
4+
import { get_first_child } from '../operations.js';
45

56
/**
67
* @param {HTMLDivElement | SVGGElement} element
@@ -9,7 +10,7 @@ import { hydrate_node, hydrating, set_hydrate_node } from '../hydration.js';
910
*/
1011
export function css_props(element, get_styles) {
1112
if (hydrating) {
12-
set_hydrate_node(/** @type {TemplateNode} */ (element.firstChild));
13+
set_hydrate_node(/** @type {TemplateNode} */ (get_first_child(element)));
1314
}
1415

1516
render_effect(() => {

packages/svelte/src/internal/client/dom/blocks/each.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,12 @@ import {
1616
set_hydrate_node,
1717
set_hydrating
1818
} from '../hydration.js';
19-
import { clear_text_content, create_text } from '../operations.js';
19+
import {
20+
clear_text_content,
21+
create_text,
22+
get_first_child,
23+
get_next_sibling
24+
} from '../operations.js';
2025
import {
2126
block,
2227
branch,
@@ -116,7 +121,7 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
116121
var parent_node = /** @type {Element} */ (node);
117122

118123
anchor = hydrating
119-
? set_hydrate_node(/** @type {Comment | Text} */ (parent_node.firstChild))
124+
? set_hydrate_node(/** @type {Comment | Text} */ (get_first_child(parent_node)))
120125
: parent_node.appendChild(create_text());
121126
}
122127

@@ -510,7 +515,7 @@ function move(item, next, anchor) {
510515
var node = /** @type {EffectNodes} */ (item.e.nodes).start;
511516

512517
while (node !== end) {
513-
var next_node = /** @type {TemplateNode} */ (node.nextSibling);
518+
var next_node = /** @type {TemplateNode} */ (get_next_sibling(node));
514519
dest.before(node);
515520
node = next_node;
516521
}

packages/svelte/src/internal/client/dom/blocks/html.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import * as w from '../../warnings.js';
88
import { hash } from '../../../../utils.js';
99
import { DEV } from 'esm-env';
1010
import { dev_current_component_function } from '../../runtime.js';
11+
import { get_first_child, get_next_sibling } from '../operations.js';
1112

1213
/**
1314
* @param {Element} element
@@ -71,7 +72,7 @@ export function html(node, get_value, svg, mathml, skip_warning) {
7172
(next.nodeType !== 8 || /** @type {Comment} */ (next).data !== '')
7273
) {
7374
last = next;
74-
next = /** @type {TemplateNode} */ (next.nextSibling);
75+
next = /** @type {TemplateNode} */ (get_next_sibling(next));
7576
}
7677

7778
if (next === null) {
@@ -98,17 +99,17 @@ export function html(node, get_value, svg, mathml, skip_warning) {
9899
var node = create_fragment_from_html(html);
99100

100101
if (svg || mathml) {
101-
node = /** @type {Element} */ (node.firstChild);
102+
node = /** @type {Element} */ (get_first_child(node));
102103
}
103104

104105
assign_nodes(
105-
/** @type {TemplateNode} */ (node.firstChild),
106+
/** @type {TemplateNode} */ (get_first_child(node)),
106107
/** @type {TemplateNode} */ (node.lastChild)
107108
);
108109

109110
if (svg || mathml) {
110-
while (node.firstChild) {
111-
anchor.before(node.firstChild);
111+
while (get_first_child(node)) {
112+
anchor.before(/** @type {Node} */ (get_first_child(node)));
112113
}
113114
} else {
114115
anchor.before(node);

packages/svelte/src/internal/client/dom/blocks/snippet.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { create_fragment_from_html } from '../reconciler.js';
1212
import { assign_nodes } from '../template.js';
1313
import * as w from '../../warnings.js';
1414
import { DEV } from 'esm-env';
15+
import { get_first_child, get_next_sibling } from '../operations.js';
1516

1617
/**
1718
* @template {(node: TemplateNode, ...args: any[]) => void} SnippetFn
@@ -89,9 +90,9 @@ export function createRawSnippet(fn) {
8990
} else {
9091
var html = snippet.render().trim();
9192
var fragment = create_fragment_from_html(html);
92-
element = /** @type {Element} */ (fragment.firstChild);
93+
element = /** @type {Element} */ (get_first_child(fragment));
9394

94-
if (DEV && (element.nextSibling !== null || element.nodeType !== 3)) {
95+
if (DEV && (get_next_sibling(element) !== null || element.nodeType !== 3)) {
9596
w.invalid_raw_snippet_render();
9697
}
9798

packages/svelte/src/internal/client/dom/blocks/svelte-element.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
set_hydrate_node,
88
set_hydrating
99
} from '../hydration.js';
10-
import { create_text } from '../operations.js';
10+
import { create_text, get_first_child } from '../operations.js';
1111
import {
1212
block,
1313
branch,
@@ -119,7 +119,7 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio
119119
// If hydrating, use the existing ssr comment as the anchor so that the
120120
// inner open and close methods can pick up the existing nodes correctly
121121
var child_anchor = /** @type {TemplateNode} */ (
122-
hydrating ? element.firstChild : element.appendChild(create_text())
122+
hydrating ? get_first_child(element) : element.appendChild(create_text())
123123
);
124124

125125
if (hydrating) {

packages/svelte/src/internal/client/dom/blocks/svelte-head.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/** @import { TemplateNode } from '#client' */
22
import { hydrate_node, hydrating, set_hydrate_node, set_hydrating } from '../hydration.js';
3-
import { create_text } from '../operations.js';
3+
import { create_text, get_first_child, get_next_sibling } from '../operations.js';
44
import { block } from '../../reactivity/effects.js';
55
import { HEAD_EFFECT } from '../../constants.js';
66
import { HYDRATION_START } from '../../../../constants.js';
@@ -32,22 +32,22 @@ export function head(render_fn) {
3232

3333
// There might be multiple head blocks in our app, so we need to account for each one needing independent hydration.
3434
if (head_anchor === undefined) {
35-
head_anchor = /** @type {TemplateNode} */ (document.head.firstChild);
35+
head_anchor = /** @type {TemplateNode} */ (get_first_child(document.head));
3636
}
3737

3838
while (
3939
head_anchor !== null &&
4040
(head_anchor.nodeType !== 8 || /** @type {Comment} */ (head_anchor).data !== HYDRATION_START)
4141
) {
42-
head_anchor = /** @type {TemplateNode} */ (head_anchor.nextSibling);
42+
head_anchor = /** @type {TemplateNode} */ (get_next_sibling(head_anchor));
4343
}
4444

4545
// If we can't find an opening hydration marker, skip hydration (this can happen
4646
// if a framework rendered body but not head content)
4747
if (head_anchor === null) {
4848
set_hydrating(false);
4949
} else {
50-
head_anchor = set_hydrate_node(/** @type {TemplateNode} */ (head_anchor.nextSibling));
50+
head_anchor = set_hydrate_node(/** @type {TemplateNode} */ (get_next_sibling(head_anchor)));
5151
}
5252
}
5353

packages/svelte/src/internal/client/dom/elements/misc.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { hydrating } from '../hydration.js';
2-
import { clear_text_content } from '../operations.js';
2+
import { clear_text_content, get_first_child } from '../operations.js';
33
import { queue_micro_task } from '../task.js';
44

55
/**
@@ -27,7 +27,7 @@ export function autofocus(dom, value) {
2727
* @returns {void}
2828
*/
2929
export function remove_textarea_child(dom) {
30-
if (hydrating && dom.firstChild !== null) {
30+
if (hydrating && get_first_child(dom) !== null) {
3131
clear_text_content(dom);
3232
}
3333
}

packages/svelte/src/internal/client/dom/hydration.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
HYDRATION_START_ELSE
88
} from '../../../constants.js';
99
import * as w from '../warnings.js';
10+
import { get_next_sibling } from './operations.js';
1011

1112
/**
1213
* Use this variable to guard everything related to hydration code so it can be treeshaken out
@@ -39,15 +40,15 @@ export function set_hydrate_node(node) {
3940
}
4041

4142
export function hydrate_next() {
42-
return set_hydrate_node(/** @type {TemplateNode} */ (hydrate_node.nextSibling));
43+
return set_hydrate_node(/** @type {TemplateNode} */ (get_next_sibling(hydrate_node)));
4344
}
4445

4546
/** @param {TemplateNode} node */
4647
export function reset(node) {
4748
if (!hydrating) return;
4849

4950
// If the node has remaining siblings, something has gone wrong
50-
if (hydrate_node.nextSibling !== null) {
51+
if (get_next_sibling(hydrate_node) !== null) {
5152
w.hydration_mismatch();
5253
throw HYDRATION_ERROR;
5354
}
@@ -90,7 +91,7 @@ export function remove_nodes() {
9091
}
9192
}
9293

93-
var next = /** @type {TemplateNode} */ (node.nextSibling);
94+
var next = /** @type {TemplateNode} */ (get_next_sibling(node));
9495
node.remove();
9596
node = next;
9697
}

packages/svelte/src/internal/client/dom/operations.js

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import { hydrate_node, hydrating, set_hydrate_node } from './hydration.js';
33
import { DEV } from 'esm-env';
44
import { init_array_prototype_warnings } from '../dev/equality.js';
5+
import { get_descriptor } from '../../shared/utils.js';
56

67
// export these for reference in the compiled code, making global name deduplication unnecessary
78
/** @type {Window} */
@@ -10,6 +11,11 @@ export var $window;
1011
/** @type {Document} */
1112
export var $document;
1213

14+
/** @type {() => Node | null} */
15+
var first_child_getter;
16+
/** @type {() => Node | null} */
17+
var next_sibling_getter;
18+
1319
/**
1420
* Initialize these lazily to avoid issues when using the runtime in a server context
1521
* where these globals are not available while avoiding a separate server entry point
@@ -23,6 +29,12 @@ export function init_operations() {
2329
$document = document;
2430

2531
var element_prototype = Element.prototype;
32+
var node_prototype = Node.prototype;
33+
34+
// @ts-ignore
35+
first_child_getter = get_descriptor(node_prototype, 'firstChild').get;
36+
// @ts-ignore
37+
next_sibling_getter = get_descriptor(node_prototype, 'nextSibling').get;
2638

2739
// the following assignments improve perf of lookups on DOM nodes
2840
// @ts-expect-error
@@ -53,6 +65,24 @@ export function create_text(value = '') {
5365
return document.createTextNode(value);
5466
}
5567

68+
/**
69+
* @template {Node} N
70+
* @param {N} node
71+
* @returns {Node | null}
72+
*/
73+
export function get_first_child(node) {
74+
return first_child_getter.call(node);
75+
}
76+
77+
/**
78+
* @template {Node} N
79+
* @param {N} node
80+
* @returns {Node | null}
81+
*/
82+
export function get_next_sibling(node) {
83+
return next_sibling_getter.call(node);
84+
}
85+
5686
/**
5787
* Don't mark this as side-effect-free, hydration needs to walk all nodes
5888
* @template {Node} N
@@ -61,10 +91,10 @@ export function create_text(value = '') {
6191
*/
6292
export function child(node) {
6393
if (!hydrating) {
64-
return node.firstChild;
94+
return get_first_child(node);
6595
}
6696

67-
var child = /** @type {TemplateNode} */ (hydrate_node.firstChild);
97+
var child = /** @type {TemplateNode} */ (get_first_child(hydrate_node));
6898

6999
// Child can be null if we have an element with a single child, like `<p>{text}</p>`, where `text` is empty
70100
if (child === null) {
@@ -84,10 +114,10 @@ export function child(node) {
84114
export function first_child(fragment, is_text) {
85115
if (!hydrating) {
86116
// when not hydrating, `fragment` is a `DocumentFragment` (the result of calling `open_frag`)
87-
var first = /** @type {DocumentFragment} */ (fragment).firstChild;
117+
var first = /** @type {DocumentFragment} */ (get_first_child(/** @type {Node} */ (fragment)));
88118

89119
// TODO prevent user comments with the empty string when preserveComments is true
90-
if (first instanceof Comment && first.data === '') return first.nextSibling;
120+
if (first instanceof Comment && first.data === '') return get_next_sibling(first);
91121

92122
return first;
93123
}
@@ -114,10 +144,10 @@ export function first_child(fragment, is_text) {
114144
*/
115145
export function sibling(node, is_text = false) {
116146
if (!hydrating) {
117-
return /** @type {TemplateNode} */ (node.nextSibling);
147+
return /** @type {TemplateNode} */ (get_next_sibling(node));
118148
}
119149

120-
var next_sibling = /** @type {TemplateNode} */ (hydrate_node.nextSibling);
150+
var next_sibling = /** @type {TemplateNode} */ (get_next_sibling(hydrate_node));
121151

122152
var type = next_sibling.nodeType;
123153

0 commit comments

Comments
 (0)