From 14ec75c5fe88e4f54ab588fa8e7ca92493cd1116 Mon Sep 17 00:00:00 2001 From: Mathias Picker <48158184+MathiasWP@users.noreply.github.com> Date: Tue, 25 Aug 2026 16:16:17 +0200 Subject: [PATCH] perf: fold SSR block-open markers into the branch's first push (#18712) One fewer `$$renderer.push` per rendered branch --- .changeset/gold-moons-shave.md | 5 ++ .../3-transform/server/visitors/EachBlock.js | 10 +++- .../3-transform/server/visitors/IfBlock.js | 8 +-- .../server/visitors/shared/utils.js | 30 +++++++++++ .../_expected/server/index.svelte.js | 3 +- .../_expected/server/index.svelte.js | 54 +++++++------------ .../_expected/server/index.svelte.js | 9 ++-- .../_expected/server/index.svelte.js | 3 +- 8 files changed, 70 insertions(+), 52 deletions(-) create mode 100644 .changeset/gold-moons-shave.md diff --git a/.changeset/gold-moons-shave.md b/.changeset/gold-moons-shave.md new file mode 100644 index 0000000000..9ee98321b7 --- /dev/null +++ b/.changeset/gold-moons-shave.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +perf: fold SSR block-open markers into the branch's first push diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/EachBlock.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/EachBlock.js index cb5a61d52f..923c4dd394 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/EachBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/EachBlock.js @@ -2,7 +2,13 @@ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types.js' */ import * as b from '#compiler/builders'; -import { block_close, block_open, block_open_else, create_child_block } from './shared/utils.js'; +import { + block_close, + block_open, + block_open_else, + create_child_block, + prepend_block_marker +} from './shared/utils.js'; /** * @param {AST.EachBlock} node @@ -51,7 +57,7 @@ export function EachBlock(node, context) { const fallback = /** @type {BlockStatement} */ (context.visit(node.fallback)); - fallback.body.unshift(b.stmt(b.call(b.id('$$renderer.push'), block_open_else))); + prepend_block_marker(fallback, /** @type {string} */ (block_open_else.value)); statements.push( b.if( diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/IfBlock.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/IfBlock.js index 2d1a22605c..ea636e2064 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/IfBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/IfBlock.js @@ -2,7 +2,7 @@ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types.js' */ import * as b from '#compiler/builders'; -import { block_close, create_child_block } from './shared/utils.js'; +import { block_close, create_child_block, prepend_block_marker } from './shared/utils.js'; /** * @param {AST.IfBlock} node @@ -10,7 +10,7 @@ import { block_close, create_child_block } from './shared/utils.js'; */ export function IfBlock(node, context) { const consequent = /** @type {BlockStatement} */ (context.visit(node.consequent)); - consequent.body.unshift(b.stmt(b.call(b.id('$$renderer.push'), b.literal(``)))); + prepend_block_marker(consequent, ``); /** @type {IfStatement} */ let if_statement = b.if(/** @type {Expression} */ (context.visit(node.test)), consequent); @@ -22,7 +22,7 @@ export function IfBlock(node, context) { // Walk the else-if chain, flattening branches for (const elseif of node.metadata.flattened ?? []) { const branch = /** @type {BlockStatement} */ (context.visit(elseif.consequent)); - branch.body.unshift(b.stmt(b.call(b.id('$$renderer.push'), b.literal(``)))); + prepend_block_marker(branch, ``); current_if = current_if.alternate = b.if( /** @type {Expression} */ (context.visit(elseif.test)), @@ -34,7 +34,7 @@ export function IfBlock(node, context) { // Handle final else (or remaining async chain) const final_alternate = alt ? /** @type {BlockStatement} */ (context.visit(alt)) : b.block([]); - final_alternate.body.unshift(b.stmt(b.call(b.id('$$renderer.push'), b.literal(``)))); + prepend_block_marker(final_alternate, ``); current_if.alternate = final_alternate; context.state.template.push( diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js index 28d2d762f9..f38ae6164c 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js @@ -180,6 +180,36 @@ export function build_template(template) { return statements; } +/** + * Prepends a hydration marker (e.g. ``) to a branch. The branch has already been + * turned into statements by `build_template`, so if it happens to start with a static + * `$$renderer.push(...)` we fold the marker into that call rather than emitting a second one. + * @param {BlockStatement} block + * @param {string} marker + */ +export function prepend_block_marker(block, marker) { + const first = block.body[0]; + + if ( + first?.type === 'ExpressionStatement' && + first.expression.type === 'CallExpression' && + first.expression.callee.type === 'Identifier' && + first.expression.callee.name === '$$renderer.push' && + first.expression.arguments.length === 1 && + first.expression.arguments[0].type === 'TemplateLiteral' + ) { + const quasi = first.expression.arguments[0].quasis[0]; + + // markers never contain characters that need escaping in a template literal + quasi.value.cooked = marker + quasi.value.cooked; + quasi.value.raw = marker + quasi.value.raw; + + return; + } + + block.body.unshift(b.stmt(b.call(b.id('$$renderer.push'), b.literal(marker)))); +} + /** * * @param {AST.Attribute['value']} value diff --git a/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_expected/server/index.svelte.js index b331875d04..71ccba4d59 100644 --- a/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_expected/server/index.svelte.js @@ -15,8 +15,7 @@ export default function Async_each_fallback_hoisting($$renderer) { $$renderer.push(async () => $.escape(await Promise.reject('This should never be reached'))); } } else { - $$renderer.push(''); - $$renderer.push(``); + $$renderer.push(``); $$renderer.push(async () => $.escape(await Promise.resolve(4))); } }); diff --git a/packages/svelte/tests/snapshot/samples/async-if-chain/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-if-chain/_expected/server/index.svelte.js index 3fc25288ad..466683b868 100644 --- a/packages/svelte/tests/snapshot/samples/async-if-chain/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/async-if-chain/_expected/server/index.svelte.js @@ -12,14 +12,11 @@ export default function Async_if_chain($$renderer) { $$renderer.async_block([$$promises[0]], ($$renderer) => { if (foo) { - $$renderer.push(''); - $$renderer.push(`foo`); + $$renderer.push(`foo`); } else if (bar) { - $$renderer.push(''); - $$renderer.push(`bar`); + $$renderer.push(`bar`); } else { - $$renderer.push(''); - $$renderer.push(`else`); + $$renderer.push(`else`); } }); @@ -27,21 +24,17 @@ export default function Async_if_chain($$renderer) { $$renderer.async_block([$$promises[0]], async ($$renderer) => { if ((await $.save(foo))()) { - $$renderer.push(''); - $$renderer.push(`foo`); + $$renderer.push(`foo`); } else if (bar) { - $$renderer.push(''); - $$renderer.push(`bar`); + $$renderer.push(`bar`); } else { $$renderer.push(''); $$renderer.child_block(async ($$renderer) => { if ((await $.save(baz))()) { - $$renderer.push(''); - $$renderer.push(`baz`); + $$renderer.push(`baz`); } else { - $$renderer.push(''); - $$renderer.push(`else`); + $$renderer.push(`else`); } }); @@ -53,21 +46,17 @@ export default function Async_if_chain($$renderer) { $$renderer.async_block([$$promises[0]], async ($$renderer) => { if ((await $.save(foo))() > 10) { - $$renderer.push(''); - $$renderer.push(`foo`); + $$renderer.push(`foo`); } else if (bar) { - $$renderer.push(''); - $$renderer.push(`bar`); + $$renderer.push(`bar`); } else { $$renderer.push(''); $$renderer.async_block([$$promises[0]], async ($$renderer) => { if ((await $.save(foo))() > 5) { - $$renderer.push(''); - $$renderer.push(`baz`); + $$renderer.push(`baz`); } else { - $$renderer.push(''); - $$renderer.push(`else`); + $$renderer.push(`else`); } }); @@ -78,31 +67,24 @@ export default function Async_if_chain($$renderer) { $$renderer.push(` `); if (simple1) { - $$renderer.push(''); - $$renderer.push(`foo`); + $$renderer.push(`foo`); } else if (simple2 > 10) { - $$renderer.push(''); - $$renderer.push(`bar`); + $$renderer.push(`bar`); } else if (complex1() * complex2 > 100) { - $$renderer.push(''); - $$renderer.push(`baz`); + $$renderer.push(`baz`); } else { - $$renderer.push(''); - $$renderer.push(`else`); + $$renderer.push(`else`); } $$renderer.push(` `); $$renderer.async_block([$$promises[0]], ($$renderer) => { if (blocking() > 10) { - $$renderer.push(''); - $$renderer.push(`foo`); + $$renderer.push(`foo`); } else if (blocking() > 5) { - $$renderer.push(''); - $$renderer.push(`bar`); + $$renderer.push(`bar`); } else { - $$renderer.push(''); - $$renderer.push(`else`); + $$renderer.push(`else`); } }); diff --git a/packages/svelte/tests/snapshot/samples/dedupe-templates/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/dedupe-templates/_expected/server/index.svelte.js index 1f4a56779e..be305165cb 100644 --- a/packages/svelte/tests/snapshot/samples/dedupe-templates/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/dedupe-templates/_expected/server/index.svelte.js @@ -4,18 +4,15 @@ export default function Dedupe_templates($$renderer, $$props) { let { a, b } = $$props; if (a) { - $$renderer.push(''); - $$renderer.push(`
hello
`); + $$renderer.push(`hello
`); } else { - $$renderer.push(''); - $$renderer.push(`hello
`); + $$renderer.push(`hello
`); } $$renderer.push(` `); if (b) { - $$renderer.push(''); - $$renderer.push(`hello
`); + $$renderer.push(`hello
`); } else { $$renderer.push(''); } diff --git a/packages/svelte/tests/snapshot/samples/select-with-rich-content/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/select-with-rich-content/_expected/server/index.svelte.js index bc346b4b0a..de22dc2f96 100644 --- a/packages/svelte/tests/snapshot/samples/select-with-rich-content/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/select-with-rich-content/_expected/server/index.svelte.js @@ -148,8 +148,7 @@ export default function Select_with_rich_content($$renderer) { $$renderer.push(`