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 4 months 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) { } else if (is_single_child_not_needing_template) {
context.visit(trimmed[0], state); context.visit(trimmed[0], state);
body.push(...state.before_init, ...state.init); 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) { } else if (trimmed.length > 0) {
const id = b.id(context.state.scope.generate('fragment')); const id = b.id(context.state.scope.generate('fragment'));

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

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

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

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

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

@ -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 { empty } from './operations.js'; import { create_text } 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';
@ -209,10 +209,11 @@ function run_scripts(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
* @param {any} value
*/ */
export function text() { export function text(value = '') {
if (!hydrating) { if (!hydrating) {
var t = empty(); var t = create_text(value + '');
assign_nodes(t, t); assign_nodes(t, t);
return t; return t;
} }
@ -221,7 +222,7 @@ export function text() {
if (node.nodeType !== 3) { if (node.nodeType !== 3) {
// if an {expression} is empty during SSR, we need to insert an empty text node // 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); set_hydrate_node(node);
} }
@ -238,7 +239,7 @@ export function comment() {
var frag = document.createDocumentFragment(); var frag = document.createDocumentFragment();
var start = document.createComment(''); var start = document.createComment('');
var anchor = empty(); var anchor = create_text();
frag.append(start, anchor); frag.append(start, anchor);
assign_nodes(start, anchor); assign_nodes(start, anchor);

@ -1,7 +1,7 @@
/** @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, 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 { 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';
@ -43,13 +43,9 @@ export function set_should_intro(value) {
*/ */
export function set_text(text, value) { export function set_text(text, value) {
// @ts-expect-error // @ts-expect-error
const prev = (text.__t ??= text.nodeValue); if (value !== (text.__t ??= text.nodeValue)) {
if (prev !== value) {
// @ts-expect-error // @ts-expect-error
text.__t = value; text.nodeValue = 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 + '';
} }
} }
@ -78,7 +74,7 @@ export function set_text(text, value) {
* @returns {Exports} * @returns {Exports}
*/ */
export function mount(component, options) { 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 }); 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`; const message = `call +636-555-3226 now`;
</script> </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 * as $ from "svelte/internal/client";
import TextInput from './Child.svelte'; import TextInput from './Child.svelte';
var root_1 = $.template(`Something`, 1);
var root = $.template(`<!> `, 1); var root = $.template(`<!> `, 1);
export default function Bind_component_snippet($$anchor) { export default function Bind_component_snippet($$anchor) {
const snippet = ($$anchor) => { const snippet = ($$anchor) => {
$.next(); $.next();
var fragment = root_1(); var text = $.text("Something");
$.append($$anchor, fragment); $.append($$anchor, text);
}; };
let value = $.source(''); let value = $.source('');
const _snippet = snippet; const _snippet = snippet;
var fragment_1 = root(); var fragment = root();
var node = $.first_child(fragment_1); var node = $.first_child(fragment);
TextInput(node, { TextInput(node, {
get value() { 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) ?? ""}`)); $.template_effect(() => $.set_text(text_1, ` value: ${$.get(value) ?? ""}`));
$.append($$anchor, fragment_1); $.append($$anchor, fragment);
} }

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

Loading…
Cancel
Save