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 <hello@rich-harris.dev>
pull/18241/head
Sean Kenneth Doherty 4 days ago committed by GitHub
parent f9440dc3ed
commit a5df6616ea
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: inline primitive constants in attribute values during SSR

@ -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));
}
/**

@ -1,9 +1,9 @@
import 'svelte/internal/disclose-version';
import * as $ from 'svelte/internal/client';
var root = $.from_html(`<h1></h1> <b></b> <button> </button> <h1></h1>`, 1);
var root = $.from_html(`<h1></h1> <b></b> <button> </button> <h1></h1> <div></div>`, 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);
}

@ -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(`<h1>Hello, world!</h1> <b>123</b> <button>Count is ${$.escape(count)}</button> <h1>Hello, world</h1>`);
$$renderer.push(`<h1>Hello, world!</h1> <b>123</b> <button>Count is ${$.escape(count)}</button> <h1>Hello, world</h1> <div${$.attr('title', `Hello, world ${$.stringify(count)} 1 ${typeof value} ${$.stringify(value)}`)}></div>`);
}

@ -1,8 +1,10 @@
<script>
let name = 'world';
let count = $state(0);
let { value } = $props();
</script>
<h1>Hello, {null}{name}!</h1>
<b>{1 ?? 'stuff'}{2 ?? 'more stuff'}{3 ?? 'even more stuff'}</b>
<button onclick={()=>count++}>Count is {count}</button>
<h1>Hello, {name ?? 'earth' ?? null}</h1>
<h1>Hello, {name ?? 'earth' ?? null}</h1>
<div title="Hello, {name} {count} {null} {1} {undefined} {typeof value} {value}"></div>

Loading…
Cancel
Save