From a5df6616ea6c8ee0984ff0fc7b04e51472f1d01d Mon Sep 17 00:00:00 2001 From: Sean Kenneth Doherty Date: Mon, 18 May 2026 13:53:10 -0500 Subject: [PATCH] fix: avoid unnecessary stringify in server attributes (#18232) ### Before submitting the PR, please make sure you do the following - [x] It's really useful if your PR references an issue where it is discussed ahead of time. Fixes #10031. - [x] Prefix your PR title with `feat:`, `fix:`, `chore:`, or `docs:`. - [x] This message body should clearly illustrate what problems it solves. - [x] Ideally, include a test that fails without this PR but passes with it. - [x] If this PR changes code within `packages/svelte/src`, add a changeset (`npx changeset`). ### What this changes Server attribute template generation currently wraps each dynamic expression in `$.stringify`, even when the compiler can prove the expression is a string or a known constant. This reuses the existing scope evaluation metadata so server output can avoid `$.stringify` for proven string/constant chunks while keeping it for possibly nullish unknown values. The updated snapshot covers a mixed attribute with a known string, mutable state, `null`, numeric/undefined constants, a known string-producing `typeof`, and an unknown prop value. ### Tests and linting - [x] `pnpm test snapshot -t nullish-coallescence-omittance` - [x] `pnpm test snapshot` - [x] `pnpm --filter svelte check` - [x] `pnpm lint` - [x] `pnpm prettier --check .changeset/slow-bikes-serve.md` --------- Co-authored-by: Rich Harris --- .changeset/slow-bikes-serve.md | 5 +++ .../server/visitors/shared/utils.js | 33 ++++++++++++------- .../_expected/client/index.svelte.js | 13 ++++++-- .../_expected/server/index.svelte.js | 5 +-- .../index.svelte | 4 ++- 5 files changed, 42 insertions(+), 18 deletions(-) create mode 100644 .changeset/slow-bikes-serve.md diff --git a/.changeset/slow-bikes-serve.md b/.changeset/slow-bikes-serve.md new file mode 100644 index 0000000000..5e1d654353 --- /dev/null +++ b/.changeset/slow-bikes-serve.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: inline primitive constants in attribute values during SSR 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 9b3ac3ad78..62e1c44094 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 @@ -229,18 +229,25 @@ export function build_attribute_value( ? node.data.replace(regex_whitespaces_strict, ' ') : node.data; } else { - expressions.push( - b.call( - '$.stringify', - transform( - /** @type {Expression} */ (context.visit(node.expression)), - node.metadata.expression - ) - ) - ); + const evaluated = context.state.scope.evaluate(node.expression); - quasi = b.quasi('', i + 1 === value.length); - quasis.push(quasi); + if (evaluated.is_known) { + quasi.value.cooked += (evaluated.value ?? '') + ''; + } else { + const expression = transform( + /** @type {Expression} */ (context.visit(node.expression)), + node.metadata.expression + ); + + expressions.push( + evaluated.is_string && evaluated.is_defined + ? expression + : b.call('$.stringify', expression) + ); + + quasi = b.quasi('', i + 1 === value.length); + quasis.push(quasi); + } } } @@ -248,7 +255,9 @@ export function build_attribute_value( quasi.value.raw = sanitize_template_string(/** @type {string} */ (quasi.value.cooked)); } - return b.template(quasis, expressions); + return expressions.length > 0 + ? b.template(quasis, expressions) + : b.literal(/** @type {string} */ (quasi.value.cooked)); } /** diff --git a/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/client/index.svelte.js index 5281a86582..837a7cd65a 100644 --- a/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/client/index.svelte.js @@ -1,9 +1,9 @@ import 'svelte/internal/disclose-version'; import * as $ from 'svelte/internal/client'; -var root = $.from_html(`

`, 1); +var root = $.from_html(`

`, 1); -export default function Nullish_coallescence_omittance($$anchor) { +export default function Nullish_coallescence_omittance($$anchor, $$props) { let name = 'world'; let count = $.state(0); var fragment = root(); @@ -23,7 +23,14 @@ export default function Nullish_coallescence_omittance($$anchor) { var h1_1 = $.sibling(button, 2); h1_1.textContent = 'Hello, world'; - $.template_effect(() => $.set_text(text, `Count is ${$.get(count) ?? ''}`)); + + var div = $.sibling(h1_1, 2); + + $.template_effect(() => { + $.set_text(text, `Count is ${$.get(count) ?? ''}`); + $.set_attribute(div, 'title', `Hello, world ${$.get(count) ?? ''} 1 ${typeof $$props.value} ${$$props.value ?? ''}`); + }); + $.delegated('click', button, () => $.update(count)); $.append($$anchor, fragment); } diff --git a/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/server/index.svelte.js index a7e580acb8..357a887c9b 100644 --- a/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/server/index.svelte.js @@ -1,8 +1,9 @@ import * as $ from 'svelte/internal/server'; -export default function Nullish_coallescence_omittance($$renderer) { +export default function Nullish_coallescence_omittance($$renderer, $$props) { let name = 'world'; let count = 0; + let { value } = $$props; - $$renderer.push(`

Hello, world!

123

Hello, world

`); + $$renderer.push(`

Hello, world!

123

Hello, world

`); } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/index.svelte b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/index.svelte index a67c574fee..ed88f5a269 100644 --- a/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/index.svelte +++ b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/index.svelte @@ -1,8 +1,10 @@

Hello, {null}{name}!

{1 ?? 'stuff'}{2 ?? 'more stuff'}{3 ?? 'even more stuff'} -

Hello, {name ?? 'earth' ?? null}

\ No newline at end of file +

Hello, {name ?? 'earth' ?? null}

+