By checking that current/previous batch are null we filter out false
positives. `reactivity_loss_tracker` is only reset after a microtask, so
if a flush happens before that, we get warnings for things we shouldn't
warn on.
Fixes#18370
---------
Co-authored-by: Rich Harris <rich.harris@vercel.com>
If a block has a sole component child, we apply an optimization to not
need a template. But if that sole child is wrapped in `$.async`, it can
happens that the block's effect is assigned the wrong end node.
In the reproduction this happens because we have two components inside a
Child which is rendered only after an async block resolves. In
`$.append` we have logic to not reassign nodes when the active effect
didn't already run, which it has by the time we come across it (because
the async work had to resolve first; we added this in
https://github.com/sveltejs/svelte/pull/17120 to prevent the opposite,
nodes being "rewinded" to an earlier position) and so the second sibling
component's `$.append` will not set its end node to the effect. As a
result the end node is wrong (too early).
To fix this we assign the nodes in `$.async`. We could also use
`compareDocumentPosition` in `$.append` to only ever go forward, but
that feels a bit heavier performance-wise.
---------
Co-authored-by: paoloricciuti <ricciutipaolo@gmail.com>
Co-authored-by: Rich Harris <rich.harris@vercel.com>
The rejects all async deriveds of a batch as `OBSOLETE`, so they don't
hang around and bail early without triggering the batch.
If we don't do this, an async derived can trigger the already done
batch, which schedules an effect that is never flushed. Because it is
never flushed the branches it touched on its way up are never cleared,
and so anything else in that subtree is now unreactive.
Mimic what `parseExpressionAt` does
---------
Co-authored-by: hjaber <hjaber@users.noreply.github.com>
Co-authored-by: Simon Holthausen <simon.holthausen@vercel.com>
Detect TypeScript `type` declarations by actually parsing instead of by
character-class blacklists, so that expressions like `{type === 'all' ?
a : b}` or `{type instanceof Foo}` aren't misclassified as malformed
declarations.
Closes#18328
---------
Co-authored-by: Simon Holthausen <simon.holthausen@vercel.com>
Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com>
### 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. In many cases, features are absent for a
reason. For large changes, please create an RFC:
https://github.com/sveltejs/rfcs
- [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`).
### Tests and linting
- [x] Run the tests with `pnpm test` and lint the project with `pnpm
lint`
Fixes#18332.
When spread props put `value` before a later `type="hidden"` on an
`<input>`, `set_attributes` currently writes the value while the element
is still a text input. Browsers sanitize text input values by removing
newlines, so the hidden input permanently loses them before `type` is
applied.
This sets an input's `type` first whenever the same spread update also
includes `value` or `__value`, so the existing value handling runs with
the final input type. The new runtime-browser sample covers both the
problematic spread-first order and the already-working type-first order.
Validation:
- `corepack pnpm lint`
- `CI=1 corepack pnpm test`
- `corepack pnpm vitest run
packages/svelte/tests/runtime-browser/test.ts -t
input-type-before-value-spread`
---------
Co-authored-by: Rich Harris <rich.harris@vercel.com>
Co-authored-by: Rich Harris <hello@rich-harris.dev>
## Summary
- detect `SvelteURLSearchParams#set` changes by comparing duplicate
value lists instead of joined strings
- update reactive subscribers when duplicate params collapse to a single
value with the same concatenated text
- cover both `SvelteURLSearchParams` and `SvelteURL.searchParams`
synchronization
## Tests
- pnpm exec vitest run
packages/svelte/src/reactivity/url-search-params.test.ts -t
"URLSearchParams.set updates when duplicate values collapse to the same
joined string"
- pnpm exec vitest run
packages/svelte/src/reactivity/url-search-params.test.ts
packages/svelte/src/reactivity/url.test.ts
---------
Co-authored-by: Rich Harris <hello@rich-harris.dev>
Byte-identical templates hoisted from different fragments, elements, or
branches now share a single `$.from_html` factory instead of each
emitting its own module-scope variable - this shrinks generated output
and avoids redundant runtime template parsing. Dedup is keyed on
`(content, flags)` and is skipped in dev mode (templates there are
wrapped in `$.add_locations`, which embeds per-call-site info).
### Before submitting the PR, please make sure you do the following
- [ ] It's really useful if your PR references an issue where it is
discussed ahead of time. In many cases, features are absent for a
reason. For large changes, please create an RFC:
https://github.com/sveltejs/rfcs
- [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`).
### Tests and linting
- [x] Run the tests with `pnpm test` and lint the project with `pnpm
lint`
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Rich Harris <rich.harris@vercel.com>
The compiler emitted an inline string array as the second argument to
`$.rest_props(...)`, and the runtime did a linear
`Array.prototype.includes` on it on every property access via the
rest-props Proxy.
The exclude list only depends on the component definition, not on the
instance, so it can be hoisted to module scope and shared by every
instance. Switching it to a `Set` at the same time makes each lookup
O(1).
For a component like `<Button ...rest />` rendered N times, this turns
one per-instance allocation (plus a linear search on every rest-prop
access) into one module-scope allocation plus O(1) lookups.
The legacy `$$restProps` path is unchanged — it mutates the exclude list
in its `deleteProperty` trap, so it can't share a hoisted Set across
instances.
---------
Co-authored-by: Rich Harris <rich.harris@vercel.com>
## Summary
The current wrapper always calls `document.createElementNS(namespace ??
NAMESPACE_HTML, tag, options)` — even for HTML elements (the >99% case),
and even when `options` would be `undefined`. Two effects compound:
1. **Route HTML elements through `createElement`** — Blink has a fast
path that skips the namespace lookup `createElementNS` always performs.
2. **Omit the trailing `undefined` argument** — V8/Blink take a slower
path for `createElementNS(ns, tag, undefined)` (and `createElement(tag,
undefined)`) than for the bare 2-arg form. This applies symmetrically to
the SVG/MathML branch, where the wrapper now also avoids the `undefined`
3rd arg.
The wrapper dispatches to the fastest call shape for every input —
`{HTML, non-HTML}` × `{with is, without is}` × no `undefined` ever.
## Affects
Every place Svelte constructs a DOM element internally:
`<svelte:element>`, `run_scripts`, the per-component `<style>` injector,
the `{@html}` wrapper, and the `<template>` element used to clone string
templates.
## Numbers
Measured in headless Chromium (Chromium 145) using the browser bench
harness from #18261, 2–3 runs, median. Lower per-call is better; higher
hz is better.
### Raw call shapes (what each shape costs in the browser)
| | hz | per-call |
| ------------------------------------------- | -------: | -------: |
| `createElement(tag)` | ~1,210k | ~0.83 µs |
| `createElement(tag, undefined)` | ~901k | ~1.11 µs |
| `createElementNS(NS_HTML, tag)` | ~913k | ~1.10 µs |
| `createElementNS(NS_HTML, tag, undefined)` | ~630k | ~1.59 µs |
| `createElement(tag, { is })` | ~484k | ~2.07 µs |
| `createElementNS(SVG_NS, tag)` | ~333k | ~3.01 µs |
| `createElementNS(SVG_NS, tag, undefined)` | ~303k | ~3.30 µs |
| `createElementNS(SVG_NS, tag, { is })` | ~245k | ~4.08 µs |
Two stable effects fall out:
- **Trailing `undefined` is consistently slower** than the bare form —
~26% on `createElement`, ~31% on `createElementNS` (HTML), ~10% on
`createElementNS` (SVG).
- **`createElement` skips a namespace lookup** that `createElementNS`
always performs — ~32% delta for equal-shape calls (`createElement(tag)`
vs `createElementNS(NS_HTML, tag)`).
### Per-case impact of this PR
| Case (namespace, `is`) | Old wrapper call | Old hz | New wrapper call
| New hz | Speedup |
| ----------------------------- |
--------------------------------------------- | --------: |
------------------------------ | --------: | ------: |
| HTML, no `is` (dominant path) | `createElementNS(NS_HTML, tag,
undefined)` | ~630k | `createElement(tag)` | ~1,210k | **~92%** (1.92×)
|
| HTML, with `is` | `createElementNS(NS_HTML, tag, { is })` | (slow) |
`createElement(tag, { is })` | ~484k | similar to above (just the
NS-skip) |
| SVG/MathML, no `is` | `createElementNS(ns, tag, undefined)` | ~303k |
`createElementNS(ns, tag)` | ~333k | **~10%** |
| SVG/MathML, with `is` | `createElementNS(ns, tag, { is })` | ~245k |
same | ~245k | no change |
No measurable change in JSDOM (both shapes route through the same JS
implementation there).
The headline gain — **~92% on the dominant HTML-no-`is` path** —
combines both effects roughly equally: dropping the `undefined` (~46%)
and switching to `createElement` (~32%).
Wrapper-vs-raw was also verified: the new wrapper measured to within ±2%
of raw `document.createElement(tag)` for the HTML-no-`is` case, so the
function-call indirection adds no measurable overhead.
See #18261 for the benchmark and methodology.
## Test plan
- [x] All 5881 runtime tests still pass (runtime-runes + runtime-legacy)
- [x] `<svelte:element xmlns={null}>` correctly falls back to HTML
(covered by existing `dynamic-element-dynamic-namespace` test)
- [x] SVG/MathML namespaces still go through `createElementNS`
- [x] Custom-element `is` option still honoured on both branches
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`current_sources` tracks the sources created within the active reaction
so that reading or writing them during that same reaction doesn't
trigger a re-run. It was an `Array` checked with
`Array.prototype.includes.call(...)` in three hot places: the
`state_unsafe_mutation` guard, `set()`'s destruction check, and
`schedule_possible_effect_self_invalidation`.
---------
Co-authored-by: Rich Harris <rich.harris@vercel.com>
Right now, if you have the following:
```svelte
{@const data = await foo}
<p>{(() => data)()}<p>
```
It will blow up during CSR, because it tries to read `data` before it
exists. The solution to this is to consider references inside closures
as blockers for those closures. This _does_ mean we'll overblock in some
circumstances, such as:
```svelte
{@const data = await foo}
<button onclick={() => data}>foo<button>
```
But I wonder if that's actually incorrect? If the user were to click
`onclick` before `data` is ready, it would blow up.
---------
Co-authored-by: Rich Harris <rich.harris@vercel.com>
If a branch is removed from the visible dom, it may be kept around
because a subsequent batch will intro it again. If we don't resume the
effects it will stay inert and therefore not react to updates anymore
Several constant lookup tables in `utils.js` were arrays searched with
`Array.prototype.includes`, which is O(n). They're queried often — per
attribute during attribute setup and SSR, per event during event
delegation, and per identifier during compilation. Switching them to
`Set` makes each lookup O(1) without changing any public behaviour.
Ref: https://github.com/sveltejs/svelte/issues/15100
Adds the tag name to the `a11y_click_events_have_key_events` warning
given when a non-interactive element has a click handler but no keyboard
events.
This already happens in `a11y_no_static_element_interactions`, and
should probably happen in many more messages.
Fixes#14413.
This keeps the temporary raw-text hydration sentinel used by dynamic
`<svelte:element>` from becoming part of the final DOM. Hydration still
gets a marker to advance through for raw-text children, but the marker
is removed after the child renderer runs, so `<style>` and `<script>`
contents stay identical to SSR output.
Tests:
- `pnpm test hydration -t svelte-head-dynamic-style`
- `pnpm test hydration`
- `pnpm test runtime-runes -t svelte-element`
- `pnpm lint`
- `git diff --check`
---------
Co-authored-by: Puneet Dixit <236133619+puneetdixit200@users.noreply.github.com>
Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com>
## Summary
The propagation walk in `handle_event_propagation` already calls
`event.composedPath()` at the start to find the entry index, but then
re-derives the same chain step-by-step via `current_target.assignedSlot
|| current_target.parentNode || .host`. Three property reads per
iteration is measurable on the hot event path.
Walk the captured `path` array by index instead.
## Notes on behavior
`composedPath()` is the spec-compliant snapshot of the dispatch chain:
- Same shadow-DOM crossings (slots and shadow roots are included for
composed events).
- Same `host` traversal (composed-path crosses shadow boundaries when
appropriate).
- Differs from the previous walk in one edge case: if a handler removes
a parent mid-dispatch, the snapshot-based walk continues through the
captured chain (matches native browser semantics — the previous
`parentNode` walk would have stopped at a null parent).
## Performance
Measured in real Chromium on a click through a 30-deep tree with five
delegated handlers: **~245k hz → ~277k hz** (~+13%, ~−12% per-event
time).
## Test plan
- [x] All 6006 runtime tests pass (runtime-runes + runtime-legacy +
runtime-browser)
- [x] Native shadow-DOM event tests (in runtime-browser) pass unchanged
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A committing/merging batch can have promises that were rejected (e.g. as
obsolete). We gotta "forward" this rejection, too, instead of just the
successful promise. At best it results in a uncaught rejection
(`async-branch-merge-obsolete`), at worst it means error boundaries are
not correctly displayed (`async-later-promise-fails-first`).
Solves the reproduction in
https://github.com/sveltejs/svelte/issues/18221#issuecomment-4507803845
This fixes the issue in
https://github.com/sveltejs/svelte/issues/18221#issuecomment-4497918414
where an error can create follup-up invariant errors. The batch errors,
has no chance to run otherwise (no more pending work) and is therefore
"dead". That means we need to unlink it otherwise it's becoming a
"zombie" and hangs around, causing unnecessary and potentially buggy (as
seen in the reproduction) merge/commit work.
I was not able to reduce the reproduction down to a test case that fails
without the fix, but it does make a related error test from #17888 work
more correctly.
While looking at
https://github.com/sveltejs/svelte/issues/18221#issuecomment-4497918414
and trying to understand how the invariant can happen I noticed that we
are not correctly filtering during commit.
- we were not ignoring deriveds
- we were not comparing the correct values (checking `source.v` instead
of the saved value) and not checking if their "is a derived" state
differs
I'm not able to come up with a test where something fails without these
(possibly because it's more about an optimization to do less reruns and
not about correctness) fixes, but they do make sense.
While looking at the reproduction in
https://github.com/sveltejs/svelte/issues/18221#issuecomment-4497918414
I immediately got greeted with a runtime error when running it in the
playground (weirdly not in the Stackblitz version). The error was that a
component expected a binding to be set in onMount, but the timing of
onMount was wrong.
Turns out it's because our logic to determine whether or not to defer
top level effects is flawed. `REACTION_RAN`, which was used previously,
is already set if the initialized component is inside an async block. We
instead check for `component_context.i` which is set to `true` on
`pop()`.
An effect could be gated behind a branch. If we don't defer + transfer
them upon merge, the branch would still be marked clean but the effect
behind it is dirty but no longer reachable. It's not reachable via mark
either because that one only concerns itself with block/async effects,
and the branch gating the effect is not guaranteed to be touched by
that.
Fixes#18249
Little sad side-effect: Since we cannot reliably know _before_ traversal
if we have no blocking pending work left (the traversal could mark an if
block falsy which contains the last blocker), we gotta undo a
performance optimization.
Thanks to
https://github.com/sveltejs/svelte/issues/17940#issuecomment-4480016550
I was finally able to isolate and reproduce a false-positive invariant
error. I had a hunch this could happen and this shows it. Essentially,
you can end up in situations where two batches are scheduled to run in
the same microtask queue flush, and if the first rebases the second the
invariant will throw, which is wrong. We can avoid this by checking if a
decrement is queued.
This started out as an investigation to get rid of the runtime code for
`{#await ...}` that deactivated a batch prior to reading the promise
function. That can result in a new batch being created if the promise
invocation happens to write a source.
Through that I discovered two other bugs:
1. The way we handle `{#await await ...}` was flawed around
SSR/hydration: On the server it would await the expression (which it
should not; `{#await await ...}` is kind of a weird special case here)
and during hydration it did not produce matching nodes, leading to
hydration fails.
2. When reading a dependency after an await expression which we add to
the current reaction, we did not deduplicate those reads. That can lead
to duplicate dependencies, which in turn can lead to bugs when
`remove_reaction` later runs. Conside this: You have `deps = [count,
unrelated, count]`. Now you do `remove_reactions(deps, 1)`, i.e. "remove
all reactions after the first one". That means the "disconnect these
from each other" logic runs for `count`, too, because it's also in the
third position, but that is wrong because it is also in the first
position, i.e. the connection should be kept.
---------
Co-authored-by: Rich Harris <rich.harris@vercel.com>
### 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>
Fixes#18206 and fixes#18207 — both are printer bugs in
`packages/svelte/src/compiler/print/index.js`.
## Changes
### Fix 1: `svelte:body` crashes the printer (#18206)
`SvelteBody` was missing from the visitor map, causing a crash with
`Error: Not implemented: SvelteBody`. Added the handler (same one-liner
pattern as `SvelteDocument`, `SvelteHead`, etc.) and added `SvelteBody`
to the `is_block_element` check so whitespace is handled consistently.
### Fix 2: Keyframe percent stops print as `0%%` (#18207)
`Percentage.value` already includes the `%` sign (captured by
`/\d+(\.\d+)?%/y`), but the printer was writing `` `${node.value}%` `` —
appending a second `%`. Changed to `context.write(node.value)` to match
the `Nth` printer pattern directly above it.
Also updated the existing `style` snapshot which had `50%%` (the bug was
silently baked in), and added dedicated test samples for both fixes.
This isn't _really_ a fix since these should never surface to the user,
but it's useful for debugging when they do, as in
https://github.com/sveltejs/kit/pull/15779. Instead of seeing `Symbol()`
we see e.g. `Symbol(uninitialized)` which makes it easier to understand
where a bug is coming from.
The logic was flawed - a teardown effect only has a teardown function
but not `fn` property, but unfreeze thought that everything with a
`teardown` needs to be unfreezed
Helps with #18221 (though likely doesn't fix it completely, at least not
the more general `batch.#roots`problems)
Turns out there are a few unavoidable cases where we have to execute the
derived even if we otherwise wouldn't, because of its lazy nature.
Fixes#18139
Our `run` function which executes top level awaits (and synchronous
statements in-between/after) did not unset the context in time in case
the function returns an async value. In that case the context was still
around until the that promise resolves, which can be too late because
unrelated things can be intertwined with the batch.
The test shows this: Without the fix, the unrelated count incrementation
would not update the view until the top level awaits in the child are
done. In the test this just shows as a delayed visual update, but it
also can result in stale roots as shown in
https://github.com/sveltejs/svelte/issues/18221#issuecomment-4470921077
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.
# Releases
## svelte@5.55.7
### Patch Changes
- fix: prevent XSS on `hydratable` from user contents
([`a16ebc67bbcf8f708360195687e1b2719463e1a4`](a16ebc67bb))
- chore: bump devalue
([#18219](https://github.com/sveltejs/svelte/pull/18219))
- fix: disallow empty attribute names during SSR
([`547853e2406a2147ad7fb5ffeba95b01bd9642da`](547853e240))
- fix: harden regex
([`d2375e2ebcab5c88feb5652f1a9d621b8f06b259`](d2375e2ebc))
- fix: move Svelte runtime properties to symbols
([`e1cbbd96441e82c9eb8a23a2903c0d06d3cda991`](e1cbbd9644))
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>