Skip to content

Commit d421838

Browse files
authored
feat: more efficient text-only fragments (#12864)
* feat: more efficient text-only fragments * set_text always receives a string now * another optimisation * revert sandbox change * fix test
1 parent d64aee7 commit d421838

File tree

15 files changed

+104
-103
lines changed

15 files changed

+104
-103
lines changed

.changeset/hot-tips-appear.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+
feat: more efficient text-only fragments

packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,14 @@ export function Fragment(node, context) {
128128
} else if (is_single_child_not_needing_template) {
129129
context.visit(trimmed[0], state);
130130
body.push(...state.before_init, ...state.init);
131+
} else if (trimmed.length === 1 && trimmed[0].type === 'Text') {
132+
const id = b.id(context.state.scope.generate('text'));
133+
body.push(
134+
b.var(id, b.call('$.text', b.literal(trimmed[0].data))),
135+
...state.before_init,
136+
...state.init
137+
);
138+
close = b.stmt(b.call('$.append', b.id('$$anchor'), id));
131139
} else if (trimmed.length > 0) {
132140
const id = b.id(context.state.scope.generate('fragment'));
133141

packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js

Lines changed: 40 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,12 @@ import {
2727
build_style_directives
2828
} from './shared/element.js';
2929
import { process_children } from './shared/fragment.js';
30-
import { build_render_statement, build_update, build_update_assignment } from './shared/utils.js';
30+
import {
31+
build_render_statement,
32+
build_template_literal,
33+
build_update,
34+
build_update_assignment
35+
} from './shared/utils.js';
3136
import { visit_event_attribute } from './shared/events.js';
3237

3338
/**
@@ -320,28 +325,43 @@ export function RegularElement(node, context) {
320325
context.visit(node, child_state);
321326
}
322327

323-
/** @type {Expression} */
324-
let arg = context.state.node;
325-
326-
// If `hydrate_node` is set inside the element, we need to reset it
327-
// after the element has been hydrated
328-
let needs_reset = trimmed.some((node) => node.type !== 'Text');
328+
// special case — if an element that only contains text, we don't need
329+
// to descend into it if the text is non-reactive
330+
const text_content =
331+
trimmed.every((node) => node.type === 'Text' || node.type === 'ExpressionTag') &&
332+
trimmed.some((node) => node.type === 'ExpressionTag') &&
333+
build_template_literal(trimmed, context.visit, child_state);
329334

330-
// The same applies if it's a `<template>` element, since we need to
331-
// set the value of `hydrate_node` to `node.content`
332-
if (node.name === 'template') {
333-
needs_reset = true;
334-
child_state.init.push(b.stmt(b.call('$.hydrate_template', arg)));
335-
arg = b.member(arg, b.id('content'));
336-
}
335+
if (text_content && !text_content.has_state) {
336+
child_state.init.push(
337+
b.stmt(
338+
b.assignment('=', b.member(context.state.node, b.id('textContent')), text_content.value)
339+
)
340+
);
341+
} else {
342+
/** @type {Expression} */
343+
let arg = context.state.node;
344+
345+
// If `hydrate_node` is set inside the element, we need to reset it
346+
// after the element has been hydrated
347+
let needs_reset = trimmed.some((node) => node.type !== 'Text');
348+
349+
// The same applies if it's a `<template>` element, since we need to
350+
// set the value of `hydrate_node` to `node.content`
351+
if (node.name === 'template') {
352+
needs_reset = true;
353+
child_state.init.push(b.stmt(b.call('$.hydrate_template', arg)));
354+
arg = b.member(arg, b.id('content'));
355+
}
337356

338-
process_children(trimmed, () => b.call('$.child', arg), true, {
339-
...context,
340-
state: child_state
341-
});
357+
process_children(trimmed, () => b.call('$.child', arg), true, {
358+
...context,
359+
state: child_state
360+
});
342361

343-
if (needs_reset) {
344-
child_state.init.push(b.stmt(b.call('$.reset', context.state.node)));
362+
if (needs_reset) {
363+
child_state.init.push(b.stmt(b.call('$.reset', context.state.node)));
364+
}
345365
}
346366

347367
if (has_declaration) {

packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js

Lines changed: 13 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -34,53 +34,26 @@ export function process_children(nodes, expression, is_element, { visit, state }
3434
state.template.push(node.raw);
3535
return;
3636
}
37+
}
3738

38-
state.template.push(' ');
39+
const text_id = get_node_id(expression(true), state, 'text');
3940

40-
const text_id = get_node_id(expression(true), state, 'text');
41+
state.template.push(' ');
4142

42-
const update = b.stmt(
43-
b.call('$.set_text', text_id, /** @type {Expression} */ (visit(node.expression, state)))
44-
);
43+
const { has_state, has_call, value } = build_template_literal(sequence, visit, state);
4544

46-
if (node.metadata.expression.has_call && !within_bound_contenteditable) {
47-
state.init.push(build_update(update));
48-
} else if (node.metadata.expression.has_state && !within_bound_contenteditable) {
49-
state.update.push(update);
50-
} else {
51-
state.init.push(
52-
b.stmt(
53-
b.assignment(
54-
'=',
55-
b.member(text_id, b.id('nodeValue')),
56-
/** @type {Expression} */ (visit(node.expression))
57-
)
58-
)
59-
);
60-
}
45+
const update = b.stmt(b.call('$.set_text', text_id, value));
6146

62-
expression = (is_text) =>
63-
is_text ? b.call('$.sibling', text_id, b.true) : b.call('$.sibling', text_id);
47+
if (has_call && !within_bound_contenteditable) {
48+
state.init.push(build_update(update));
49+
} else if (has_state && !within_bound_contenteditable) {
50+
state.update.push(update);
6451
} else {
65-
const text_id = get_node_id(expression(true), state, 'text');
66-
67-
state.template.push(' ');
68-
69-
const { has_state, has_call, value } = build_template_literal(sequence, visit, state);
70-
71-
const update = b.stmt(b.call('$.set_text', text_id, value));
72-
73-
if (has_call && !within_bound_contenteditable) {
74-
state.init.push(build_update(update));
75-
} else if (has_state && !within_bound_contenteditable) {
76-
state.update.push(update);
77-
} else {
78-
state.init.push(b.stmt(b.assignment('=', b.member(text_id, b.id('nodeValue')), value)));
79-
}
80-
81-
expression = (is_text) =>
82-
is_text ? b.call('$.sibling', text_id, b.true) : b.call('$.sibling', text_id);
52+
state.init.push(b.stmt(b.assignment('=', b.member(text_id, b.id('nodeValue')), value)));
8353
}
54+
55+
expression = (is_text) =>
56+
is_text ? b.call('$.sibling', text_id, b.true) : b.call('$.sibling', text_id);
8457
}
8558

8659
for (let i = 0; i < nodes.length; i += 1) {

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
set_hydrate_node,
1717
set_hydrating
1818
} from '../hydration.js';
19-
import { clear_text_content, empty } from '../operations.js';
19+
import { clear_text_content, create_text } from '../operations.js';
2020
import {
2121
block,
2222
branch,
@@ -117,7 +117,7 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
117117

118118
anchor = hydrating
119119
? set_hydrate_node(/** @type {Comment | Text} */ (parent_node.firstChild))
120-
: parent_node.appendChild(empty());
120+
: parent_node.appendChild(create_text());
121121
}
122122

123123
if (hydrating) {

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 { empty } from '../operations.js';
10+
import { create_text } 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(empty())
122+
hydrating ? element.firstChild : element.appendChild(create_text())
123123
);
124124

125125
if (hydrating) {

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

Lines changed: 2 additions & 2 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 { empty } from '../operations.js';
3+
import { create_text } from '../operations.js';
44
import { block } from '../../reactivity/effects.js';
55
import { HEAD_EFFECT } from '../../constants.js';
66
import { HYDRATION_START } from '../../../../constants.js';
@@ -52,7 +52,7 @@ export function head(render_fn) {
5252
}
5353

5454
if (!hydrating) {
55-
anchor = document.head.appendChild(empty());
55+
anchor = document.head.appendChild(create_text());
5656
}
5757

5858
try {

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

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,12 @@ export function init_operations() {
4545
}
4646
}
4747

48-
/** @returns {Text} */
49-
export function empty() {
50-
return document.createTextNode('');
48+
/**
49+
* @param {string} value
50+
* @returns {Text}
51+
*/
52+
export function create_text(value = '') {
53+
return document.createTextNode(value);
5154
}
5255

5356
/**
@@ -65,7 +68,7 @@ export function child(node) {
6568

6669
// Child can be null if we have an element with a single child, like `<p>{text}</p>`, where `text` is empty
6770
if (child === null) {
68-
child = hydrate_node.appendChild(empty());
71+
child = hydrate_node.appendChild(create_text());
6972
}
7073

7174
set_hydrate_node(child);
@@ -92,7 +95,7 @@ export function first_child(fragment, is_text) {
9295
// if an {expression} is empty during SSR, there might be no
9396
// text node to hydrate — we must therefore create one
9497
if (is_text && hydrate_node?.nodeType !== 3) {
95-
var text = empty();
98+
var text = create_text();
9699

97100
hydrate_node?.before(text);
98101
set_hydrate_node(text);
@@ -121,7 +124,7 @@ export function sibling(node, is_text = false) {
121124
// if a sibling {expression} is empty during SSR, there might be no
122125
// text node to hydrate — we must therefore create one
123126
if (is_text && type !== 3) {
124-
var text = empty();
127+
var text = create_text();
125128
next_sibling?.before(text);
126129
set_hydrate_node(text);
127130
return text;

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

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/** @import { Effect, EffectNodes, TemplateNode } from '#client' */
22
import { hydrate_next, hydrate_node, hydrating, set_hydrate_node } from './hydration.js';
3-
import { empty } from './operations.js';
3+
import { create_text } from './operations.js';
44
import { create_fragment_from_html } from './reconciler.js';
55
import { current_effect } from '../runtime.js';
66
import { TEMPLATE_FRAGMENT, TEMPLATE_USE_IMPORT_NODE } from '../../../constants.js';
@@ -209,10 +209,11 @@ function run_scripts(node) {
209209

210210
/**
211211
* Don't mark this as side-effect-free, hydration needs to walk all nodes
212+
* @param {any} value
212213
*/
213-
export function text() {
214+
export function text(value = '') {
214215
if (!hydrating) {
215-
var t = empty();
216+
var t = create_text(value + '');
216217
assign_nodes(t, t);
217218
return t;
218219
}
@@ -221,7 +222,7 @@ export function text() {
221222

222223
if (node.nodeType !== 3) {
223224
// if an {expression} is empty during SSR, we need to insert an empty text node
224-
node.before((node = empty()));
225+
node.before((node = create_text()));
225226
set_hydrate_node(node);
226227
}
227228

@@ -238,7 +239,7 @@ export function comment() {
238239

239240
var frag = document.createDocumentFragment();
240241
var start = document.createComment('');
241-
var anchor = empty();
242+
var anchor = create_text();
242243
frag.append(start, anchor);
243244

244245
assign_nodes(start, anchor);

packages/svelte/src/internal/client/render.js

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/** @import { ComponentContext, Effect, EffectNodes, TemplateNode } from '#client' */
22
/** @import { Component, ComponentType, SvelteComponent } from '../../index.js' */
33
import { DEV } from 'esm-env';
4-
import { clear_text_content, empty, init_operations } from './dom/operations.js';
4+
import { clear_text_content, create_text, init_operations } from './dom/operations.js';
55
import { HYDRATION_END, HYDRATION_ERROR, HYDRATION_START } from '../../constants.js';
66
import { push, pop, current_component_context, current_effect } from './runtime.js';
77
import { effect_root, branch } from './reactivity/effects.js';
@@ -43,13 +43,9 @@ export function set_should_intro(value) {
4343
*/
4444
export function set_text(text, value) {
4545
// @ts-expect-error
46-
const prev = (text.__t ??= text.nodeValue);
47-
48-
if (prev !== value) {
46+
if (value !== (text.__t ??= text.nodeValue)) {
4947
// @ts-expect-error
50-
text.__t = value;
51-
// It's faster to make the value a string rather than passing a non-string to nodeValue
52-
text.nodeValue = value == null ? '' : value + '';
48+
text.nodeValue = text.__t = value;
5349
}
5450
}
5551

@@ -78,7 +74,7 @@ export function set_text(text, value) {
7874
* @returns {Exports}
7975
*/
8076
export function mount(component, options) {
81-
const anchor = options.anchor ?? options.target.appendChild(empty());
77+
const anchor = options.anchor ?? options.target.appendChild(create_text());
8278
return _mount(component, { ...options, anchor });
8379
}
8480

0 commit comments

Comments
 (0)