perf: emit `$.only_child` for elements with a single child (#18717)

`<p>{text}</p>` and friends compile to `var x = $.child(p, true);
$.reset(p);`. Across the 4,020 components in `packages/svelte/tests`,
that exact pair occurs 1,885 times, more than any other adjacent
statement pair.

`$.only_child` does both, reducing the number of statements in the compiled output
pull/18712/head
Mathias Picker 4 days ago committed by GitHub
parent 6b99f9fb52
commit 34489c10df
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
perf: emit `$.only_child` for elements with a single child

@ -1,4 +1,4 @@
/** @import { ArrayExpression, Expression, ExpressionStatement, Identifier, MemberExpression, ObjectExpression } from 'estree' */ /** @import { ArrayExpression, Expression, ExpressionStatement, Identifier, MemberExpression, ObjectExpression, Statement } from 'estree' */
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { ComponentClientTransformState, ComponentContext } from '../types' */ /** @import { ComponentClientTransformState, ComponentContext } from '../types' */
/** @import { Scope } from '../../../scope' */ /** @import { Scope } from '../../../scope' */
@ -432,7 +432,7 @@ export function RegularElement(node, context) {
state: child_state state: child_state
}); });
if (needs_reset) { if (needs_reset && !fold_reset_into_child(child_state.init, context.state.node)) {
child_state.init.push(b.stmt(b.call('$.reset', context.state.node))); child_state.init.push(b.stmt(b.call('$.reset', context.state.node)));
} }
} }
@ -751,3 +751,39 @@ function build_element_special_value_attribute(
state.init.push(b.stmt(b.call('$.init_select', node_id))); state.init.push(b.stmt(b.call('$.init_select', node_id)));
} }
} }
/**
* `<p>{text}</p>` and friends produce `var x = $.child(p, true); $.reset(p);`. That pair is
* by far the most common shape in compiled output, and `$.only_child` does both, so fold the
* two together when the `$.child(...)` is the last thing we emitted for this element.
* @param {Statement[]} init
* @param {Expression} node_id
* @returns {boolean} whether the reset was folded in
*/
function fold_reset_into_child(init, node_id) {
const last = init.at(-1);
if (
node_id?.type !== 'Identifier' ||
last?.type !== 'VariableDeclaration' ||
last.declarations.length !== 1
) {
return false;
}
const call = last.declarations[0].init;
if (
call?.type !== 'CallExpression' ||
call.callee.type !== 'Identifier' ||
call.callee.name !== '$.child' ||
call.arguments[0]?.type !== 'Identifier' ||
call.arguments[0].name !== node_id.name
) {
return false;
}
call.callee = b.id('$.only_child');
return true;
}

@ -1,5 +1,5 @@
/** @import { Effect, TemplateNode } from '#client' */ /** @import { Effect, TemplateNode } from '#client' */
import { hydrate_node, hydrating, set_hydrate_node } from './hydration.js'; import { hydrate_node, hydrating, reset, set_hydrate_node } from './hydration.js';
import { DEV } from 'esm-env'; import { DEV } from 'esm-env';
import { init_array_prototype_warnings } from '../dev/equality.js'; import { init_array_prototype_warnings } from '../dev/equality.js';
import { get_descriptor, is_extensible } from '../../shared/utils.js'; import { get_descriptor, is_extensible } from '../../shared/utils.js';
@ -165,6 +165,26 @@ export function first_child(node, is_text = false) {
return hydrate_node; return hydrate_node;
} }
/**
* `child`, for the very common case of an element with exactly one child. Resetting the
* hydration cursor is part of the same step, so the compiler doesn't have to emit a
* separate `reset` call for every `<p>{text}</p>` in an app.
* Don't mark this as side-effect-free, hydration needs to walk all nodes
* @param {TemplateNode} node
* @param {boolean} [is_text]
* @returns {TemplateNode | null}
*/
export function only_child(node, is_text = false) {
if (!hydrating) {
return get_first_child(node);
}
var first = child(node, is_text);
reset(node);
return first;
}
/** /**
* 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 {TemplateNode} node * @param {TemplateNode} node

@ -164,6 +164,7 @@ export { proxy } from './proxy.js';
export { create_custom_element } from './dom/elements/custom-element.js'; export { create_custom_element } from './dom/elements/custom-element.js';
export { export {
child, child,
only_child,
first_child, first_child,
sibling, sibling,
$window as window, $window as window,

@ -31,9 +31,8 @@ export default function Async_const($$anchor) {
]); ]);
var p = root(); var p = root();
var text = $.child(p, true); var text = $.only_child(p, true);
$.reset(p);
$.template_effect(() => $.set_text(text, $.get(b)), void 0, void 0, [promises[1]]); $.template_effect(() => $.set_text(text, $.get(b)), void 0, void 0, [promises[1]]);
$.append($$anchor, p); $.append($$anchor, p);
}; };

@ -8,9 +8,8 @@ export default function Async_top_level_inspect_server($$anchor) {
var data; var data;
var $$promises = $.run([async () => data = await Promise.resolve(42), () => void 0]); var $$promises = $.run([async () => data = await Promise.resolve(42), () => void 0]);
var p = root(); var p = root();
var text = $.child(p, true); var text = $.only_child(p, true);
$.reset(p);
$.template_effect(() => $.set_text(text, data), void 0, void 0, [$$promises[1]]); $.template_effect(() => $.set_text(text, data), void 0, void 0, [$$promises[1]]);
$.append($$anchor, p); $.append($$anchor, p);
} }

@ -13,10 +13,7 @@ export default function Await_block_scope($$anchor) {
var fragment = root(); var fragment = root();
var button = $.first_child(fragment); var button = $.first_child(fragment);
var text = $.child(button); var text = $.only_child(button);
$.reset(button);
var node = $.sibling(button, 2); var node = $.sibling(button, 2);
$.await(node, () => $.get(promise), null, ($$anchor, counter) => {}); $.await(node, () => $.get(promise), null, ($$anchor, counter) => {});

@ -16,10 +16,7 @@ export default function Nullish_coallescence_omittance($$anchor, $$props) {
b.textContent = '123'; b.textContent = '123';
var button = $.sibling(b, 2); var button = $.sibling(b, 2);
var text = $.child(button); var text = $.only_child(button);
$.reset(button);
var h1_1 = $.sibling(button, 2); var h1_1 = $.sibling(button, 2);
h1_1.textContent = 'Hello, world'; h1_1.textContent = 'Hello, world';

@ -64,10 +64,7 @@ export default function Select_with_rich_content($$anchor) {
$.each(select_1, 5, () => items, $.index, ($$anchor, item) => { $.each(select_1, 5, () => items, $.index, ($$anchor, item) => {
var option_5 = root_4(); var option_5 = root_4();
var text = $.child(option_5, true); var text = $.only_child(option_5, true);
$.reset(option_5);
var option_5_value = {}; var option_5_value = {};
$.template_effect(() => { $.template_effect(() => {
@ -127,10 +124,7 @@ export default function Select_with_rich_content($$anchor) {
$.each(select_5, 5, () => items, $.index, ($$anchor, item) => { $.each(select_5, 5, () => items, $.index, ($$anchor, item) => {
const x = $.derived_safe_equal(() => $.get(item) * 2); const x = $.derived_safe_equal(() => $.get(item) * 2);
var option_8 = root_4(); var option_8 = root_4();
var text_1 = $.child(option_8, true); var text_1 = $.only_child(option_8, true);
$.reset(option_8);
var option_8_value = {}; var option_8_value = {};
$.template_effect(() => { $.template_effect(() => {
@ -165,10 +159,7 @@ export default function Select_with_rich_content($$anchor) {
$.each(optgroup_1, 5, () => items, $.index, ($$anchor, item) => { $.each(optgroup_1, 5, () => items, $.index, ($$anchor, item) => {
var option_10 = root_4(); var option_10 = root_4();
var text_2 = $.child(option_10, true); var text_2 = $.only_child(option_10, true);
$.reset(option_10);
var option_10_value = {}; var option_10_value = {};
$.template_effect(() => { $.template_effect(() => {
@ -208,9 +199,8 @@ export default function Select_with_rich_content($$anchor) {
var anchor_4 = $.child(option_12); var anchor_4 = $.child(option_12);
var fragment_5 = option_content_3(); var fragment_5 = option_content_3();
var span = $.first_child(fragment_5); var span = $.first_child(fragment_5);
var text_3 = $.child(span, true); var text_3 = $.only_child(span, true);
$.reset(span);
$.template_effect(() => $.set_text(text_3, $.get(item))); $.template_effect(() => $.set_text(text_3, $.get(item)));
$.append(anchor_4, fragment_5); $.append(anchor_4, fragment_5);
}); });
@ -230,10 +220,7 @@ export default function Select_with_rich_content($$anchor) {
$.each(node_4, 1, () => items, $.index, ($$anchor, item) => { $.each(node_4, 1, () => items, $.index, ($$anchor, item) => {
var option_13 = root_4(); var option_13 = root_4();
var text_4 = $.child(option_13, true); var text_4 = $.only_child(option_13, true);
$.reset(option_13);
var option_13_value = {}; var option_13_value = {};
$.template_effect(() => { $.template_effect(() => {

@ -7,10 +7,7 @@ export default function Skip_static_subtree($$anchor, $$props) {
var fragment = root(); var fragment = root();
var main = $.sibling($.first_child(fragment), 2); var main = $.sibling($.first_child(fragment), 2);
var h1 = $.child(main); var h1 = $.child(main);
var text = $.child(h1, true); var text = $.only_child(h1, true);
$.reset(h1);
var node = $.sibling(h1, 10); var node = $.sibling(h1, 10);
$.html(node, () => $$props.content); $.html(node, () => $$props.content);

@ -16,9 +16,8 @@ export default function Text_nodes_deriveds($$anchor) {
} }
var p = root(); var p = root();
var text = $.child(p); var text = $.only_child(p);
$.reset(p);
$.template_effect(($0, $1) => $.set_text(text, `${$0 ?? ''}${$1 ?? ''}`), [() => text1(), () => text2()]); $.template_effect(($0, $1) => $.set_text(text, `${$0 ?? ''}${$1 ?? ''}`), [() => text1(), () => text2()]);
$.append($$anchor, p); $.append($$anchor, p);
} }
Loading…
Cancel
Save