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
pull/12865/head
Rich Harris 1 month ago committed by GitHub
parent d64aee7432
commit d421838272
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: more efficient text-only fragments

@ -128,6 +128,14 @@ export function Fragment(node, context) {
} else if (is_single_child_not_needing_template) {
context.visit(trimmed[0], state);
body.push(...state.before_init, ...state.init);
} else if (trimmed.length === 1 && trimmed[0].type === 'Text') {
const id = b.id(context.state.scope.generate('text'));
body.push(
b.var(id, b.call('$.text', b.literal(trimmed[0].data))),
...state.before_init,
...state.init
);
close = b.stmt(b.call('$.append', b.id('$$anchor'), id));
} else if (trimmed.length > 0) {
const id = b.id(context.state.scope.generate('fragment'));

@ -27,7 +27,12 @@ import {
build_style_directives
} from './shared/element.js';
import { process_children } from './shared/fragment.js';
import { build_render_statement, build_update, build_update_assignment } from './shared/utils.js';
import {
build_render_statement,
build_template_literal,
build_update,
build_update_assignment
} from './shared/utils.js';
import { visit_event_attribute } from './shared/events.js';
/**
@ -320,28 +325,43 @@ export function RegularElement(node, context) {
context.visit(node, child_state);
}
/** @type {Expression} */
let arg = context.state.node;
// If `hydrate_node` is set inside the element, we need to reset it
// after the element has been hydrated
let needs_reset = trimmed.some((node) => node.type !== 'Text');
// special case — if an element that only contains text, we don't need
// to descend into it if the text is non-reactive
const text_content =
trimmed.every((node) => node.type === 'Text' || node.type === 'ExpressionTag') &&
trimmed.some((node) => node.type === 'ExpressionTag') &&
build_template_literal(trimmed, context.visit, child_state);
// The same applies if it's a `<template>` element, since we need to
// set the value of `hydrate_node` to `node.content`
if (node.name === 'template') {
needs_reset = true;
child_state.init.push(b.stmt(b.call('$.hydrate_template', arg)));
arg = b.member(arg, b.id('content'));
}
if (text_content && !text_content.has_state) {
child_state.init.push(
b.stmt(
b.assignment('=', b.member(context.state.node, b.id('textContent')), text_content.value)
)
);
} else {
/** @type {Expression} */
let arg = context.state.node;
// If `hydrate_node` is set inside the element, we need to reset it
// after the element has been hydrated
let needs_reset = trimmed.some((node) => node.type !== 'Text');
// The same applies if it's a `<template>` element, since we need to
// set the value of `hydrate_node` to `node.content`
if (node.name === 'template') {
needs_reset = true;
child_state.init.push(b.stmt(b.call('$.hydrate_template', arg)));
arg = b.member(arg, b.id('content'));
}
process_children(trimmed, () => b.call('$.child', arg), true, {
...context,
state: child_state
});
process_children(trimmed, () => b.call('$.child', arg), true, {
...context,
state: child_state
});
if (needs_reset) {
child_state.init.push(b.stmt(b.call('$.reset', context.state.node)));
if (needs_reset) {
child_state.init.push(b.stmt(b.call('$.reset', context.state.node)));
}
}
if (has_declaration) {

@ -34,53 +34,26 @@ export function process_children(nodes, expression, is_element, { visit, state }
state.template.push(node.raw);
return;
}
}
state.template.push(' ');
const text_id = get_node_id(expression(true), state, 'text');
const text_id = get_node_id(expression(true), state, 'text');
state.template.push(' ');
const update = b.stmt(
b.call('$.set_text', text_id, /** @type {Expression} */ (visit(node.expression, state)))
);
const { has_state, has_call, value } = build_template_literal(sequence, visit, state);
if (node.metadata.expression.has_call && !within_bound_contenteditable) {
state.init.push(build_update(update));
} else if (node.metadata.expression.has_state && !within_bound_contenteditable) {
state.update.push(update);
} else {
state.init.push(
b.stmt(
b.assignment(
'=',
b.member(text_id, b.id('nodeValue')),
/** @type {Expression} */ (visit(node.expression))
)
)
);
}
const update = b.stmt(b.call('$.set_text', text_id, value));
expression = (is_text) =>
is_text ? b.call('$.sibling', text_id, b.true) : b.call('$.sibling', text_id);
if (has_call && !within_bound_contenteditable) {
state.init.push(build_update(update));
} else if (has_state && !within_bound_contenteditable) {
state.update.push(update);
} else {
const text_id = get_node_id(expression(true), state, 'text');
state.template.push(' ');
const { has_state, has_call, value } = build_template_literal(sequence, visit, state);
const update = b.stmt(b.call('$.set_text', text_id, value));
if (has_call && !within_bound_contenteditable) {
state.init.push(build_update(update));
} else if (has_state && !within_bound_contenteditable) {
state.update.push(update);
} else {
state.init.push(b.stmt(b.assignment('=', b.member(text_id, b.id('nodeValue')), value)));
}
expression = (is_text) =>
is_text ? b.call('$.sibling', text_id, b.true) : b.call('$.sibling', text_id);
state.init.push(b.stmt(b.assignment('=', b.member(text_id, b.id('nodeValue')), value)));
}
expression = (is_text) =>
is_text ? b.call('$.sibling', text_id, b.true) : b.call('$.sibling', text_id);
}
for (let i = 0; i < nodes.length; i += 1) {

@ -16,7 +16,7 @@ import {
set_hydrate_node,
set_hydrating
} from '../hydration.js';
import { clear_text_content, empty } from '../operations.js';
import { clear_text_content, create_text } from '../operations.js';
import {
block,
branch,
@ -117,7 +117,7 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
anchor = hydrating
? set_hydrate_node(/** @type {Comment | Text} */ (parent_node.firstChild))
: parent_node.appendChild(empty());
: parent_node.appendChild(create_text());
}
if (hydrating) {

@ -7,7 +7,7 @@ import {
set_hydrate_node,
set_hydrating
} from '../hydration.js';
import { empty } from '../operations.js';
import { create_text } 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(empty())
hydrating ? element.firstChild : 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 { empty } from '../operations.js';
import { create_text } from '../operations.js';
import { block } from '../../reactivity/effects.js';
import { HEAD_EFFECT } from '../../constants.js';
import { HYDRATION_START } from '../../../../constants.js';
@ -52,7 +52,7 @@ export function head(render_fn) {
}
if (!hydrating) {
anchor = document.head.appendChild(empty());
anchor = document.head.appendChild(create_text());
}
try {

@ -45,9 +45,12 @@ export function init_operations() {
}
}
/** @returns {Text} */
export function empty() {
return document.createTextNode('');
/**
* @param {string} value
* @returns {Text}
*/
export function create_text(value = '') {
return document.createTextNode(value);
}
/**
@ -65,7 +68,7 @@ export function child(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) {
child = hydrate_node.appendChild(empty());
child = hydrate_node.appendChild(create_text());
}
set_hydrate_node(child);
@ -92,7 +95,7 @@ export function first_child(fragment, is_text) {
// if an {expression} is empty during SSR, there might be no
// text node to hydrate — we must therefore create one
if (is_text && hydrate_node?.nodeType !== 3) {
var text = empty();
var text = create_text();
hydrate_node?.before(text);
set_hydrate_node(text);
@ -121,7 +124,7 @@ export function sibling(node, is_text = false) {
// if a sibling {expression} is empty during SSR, there might be no
// text node to hydrate — we must therefore create one
if (is_text && type !== 3) {
var text = empty();
var text = create_text();
next_sibling?.before(text);
set_hydrate_node(text);
return text;

@ -1,6 +1,6 @@
/** @import { Effect, EffectNodes, TemplateNode } from '#client' */
import { hydrate_next, hydrate_node, hydrating, set_hydrate_node } from './hydration.js';
import { empty } from './operations.js';
import { create_text } 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';
@ -209,10 +209,11 @@ function run_scripts(node) {
/**
* Don't mark this as side-effect-free, hydration needs to walk all nodes
* @param {any} value
*/
export function text() {
export function text(value = '') {
if (!hydrating) {
var t = empty();
var t = create_text(value + '');
assign_nodes(t, t);
return t;
}
@ -221,7 +222,7 @@ export function text() {
if (node.nodeType !== 3) {
// if an {expression} is empty during SSR, we need to insert an empty text node
node.before((node = empty()));
node.before((node = create_text()));
set_hydrate_node(node);
}
@ -238,7 +239,7 @@ export function comment() {
var frag = document.createDocumentFragment();
var start = document.createComment('');
var anchor = empty();
var anchor = create_text();
frag.append(start, anchor);
assign_nodes(start, anchor);

@ -1,7 +1,7 @@
/** @import { ComponentContext, Effect, EffectNodes, TemplateNode } from '#client' */
/** @import { Component, ComponentType, SvelteComponent } from '../../index.js' */
import { DEV } from 'esm-env';
import { clear_text_content, empty, init_operations } from './dom/operations.js';
import { clear_text_content, create_text, 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';
@ -43,13 +43,9 @@ export function set_should_intro(value) {
*/
export function set_text(text, value) {
// @ts-expect-error
const prev = (text.__t ??= text.nodeValue);
if (prev !== value) {
if (value !== (text.__t ??= text.nodeValue)) {
// @ts-expect-error
text.__t = value;
// It's faster to make the value a string rather than passing a non-string to nodeValue
text.nodeValue = value == null ? '' : value + '';
text.nodeValue = text.__t = value;
}
}
@ -78,7 +74,7 @@ export function set_text(text, value) {
* @returns {Exports}
*/
export function mount(component, options) {
const anchor = options.anchor ?? options.target.appendChild(empty());
const anchor = options.anchor ?? options.target.appendChild(create_text());
return _mount(component, { ...options, anchor });
}

@ -1 +1 @@
<h1>call +636-555-3226 now</h1>
<h1>call +636-555-3226 now<span>!</span></h1>

@ -1 +1 @@
<!--[--><h1>call <a href="tel:+636-555-3226">+636-555-3226</a> now</h1><!--]-->
<!--[--><h1>call <a href="tel:+636-555-3226">+636-555-3226</a> now<span>!</span></h1><!--]-->

@ -2,4 +2,4 @@
const message = `call +636-555-3226 now`;
</script>
<h1>{message}</h1>
<h1>{message}<span>!</span></h1>

@ -2,22 +2,21 @@ import "svelte/internal/disclose-version";
import * as $ from "svelte/internal/client";
import TextInput from './Child.svelte';
var root_1 = $.template(`Something`, 1);
var root = $.template(`<!> `, 1);
export default function Bind_component_snippet($$anchor) {
const snippet = ($$anchor) => {
$.next();
var fragment = root_1();
var text = $.text("Something");
$.append($$anchor, fragment);
$.append($$anchor, text);
};
let value = $.source('');
const _snippet = snippet;
var fragment_1 = root();
var node = $.first_child(fragment_1);
var fragment = root();
var node = $.first_child(fragment);
TextInput(node, {
get value() {
@ -28,8 +27,8 @@ export default function Bind_component_snippet($$anchor) {
}
});
var text = $.sibling(node, true);
var text_1 = $.sibling(node, true);
$.template_effect(() => $.set_text(text, ` value: ${$.get(value) ?? ""}`));
$.append($$anchor, fragment_1);
$.template_effect(() => $.set_text(text_1, ` value: ${$.get(value) ?? ""}`));
$.append($$anchor, fragment);
}

@ -1,7 +1,7 @@
import "svelte/internal/disclose-version";
import * as $ from "svelte/internal/client";
var root = $.template(`<p> </p> <p> </p> <!>`, 1);
var root = $.template(`<p></p> <p></p> <!>`, 1);
export default function Purity($$anchor) {
let min = 0;
@ -10,16 +10,12 @@ export default function Purity($$anchor) {
let value = 'hello';
var fragment = root();
var p = $.first_child(fragment);
var text = $.child(p);
text.nodeValue = Math.max(min, Math.min(max, number));
$.reset(p);
p.textContent = `${Math.max(min, Math.min(max, number)) ?? ""}`;
var p_1 = $.sibling($.sibling(p, true));
var text_1 = $.child(p_1);
text_1.nodeValue = location.href;
$.reset(p_1);
p_1.textContent = `${location.href ?? ""}`;
var node = $.sibling($.sibling(p_1, true));

Loading…
Cancel
Save