i don't know how to stem the flood of slop PRs but maybe this will help
a tiny bit
---------
Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com>
Simon thinks it was because he thought that committing the fork != committing the batch, and that this could result in an error boundary showing up too late. But that's not the case
## 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>
## Summary
Adds a [Browser
support](https://github.com/MathiasWP/svelte/blob/document-pipeline-draft/documentation/docs/07-misc/05-browser-support.md)
page listing the minimum browser versions Svelte's runtime and compiler
output require. The page (headline floor + per-feature requirements) is
auto-generated by `packages/svelte/scripts/generate-browser-support.js`,
which scans the code Svelte actually ships against the
[web-features](https://github.com/web-platform-dx/web-features) Baseline
dataset and resolves results to concrete browser versions via
[baseline-browser-mapping](https://github.com/web-platform-dx/baseline-browser-mapping).
CI fails if a PR moves the floor without regenerating the page.
For 5.55.9 the floor is **Baseline 2020**: Chrome/Edge 87, Firefox 83,
Safari 14.
## How it works
1. **Headline floor.** Each runtime entry from `pkg.exports` is bundled
with rollup using production conditions, then walked with TypeScript's
compiler API + TypeChecker. The walker (`browser-support.detector.js`)
flags any web-features ID the runtime references; the highest Baseline
year among them is the floor.
2. **Per-feature requirements.** Every user-facing surface is enumerated
programmatically — every `bind:*` from `binding_properties`, every
public subpackage export discovered via `pkg.exports` + `import * as
ns`, every rune from `RUNES`, and directives (`transition:` / `animate:`
/ `use:` / `@attach` / `{@html}` / custom elements). Each gets a
fixture, compiled and bundled like user code, then scanned. A row
appears only when the fixture's floor exceeds the runtime floor.
3. **Supplemental rules.** A handful of APIs `web-features` doesn't
catalogue (`getComputedStyle(...).zoom`, `box:
'device-pixel-content-box'`) are detected with the same type-aware
walker via a small `register_extra_rules` list with inline
justifications.
4. **Ignore sets.** `SAFE_TO_IGNORE` covers dataset bugs and
feature-detected APIs with graceful fallbacks. `BEHAVIORAL_IGNORE` hides
APIs reached only via specific user code from the headline (they still
surface as conditional rows); staleness-checked, so CI fails if an entry
no longer exists in the runtime.
Resolves#18198. Provides an answer to the long-standing #558.
## Test plan
- [ ] \`pnpm --filter svelte generate:browser-support\` regenerates the
page idempotently
- [ ] CI fails when a code change bumps the floor without regenerating
the page
- [ ] Page renders correctly on svelte.dev
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Rich Harris <rich.harris@vercel.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>
Merged #18298 a bit too soon - there's still a situation which can
corrput the linked list: If we merge one batch into another, it will
unlink the batch immediately. But `discard` also calls it, running the
unlink logic again, which can corrupt the linked list.
Sketch:
1. Batch A is pending.
2. Batch B starts later, intersects A, resolves first, and merges into
A.
3. B is unlinked immediately, but `A.oncommit(() => B.discard())`
remains.
4. Independent batch C is created while A is still pending and is linked
after A.
5. A commits, calls `B.discard()`, and B's stale `#prev`/`#next` can set
`A.#next = null` / `last_batch = A`, disconnecting C.
Not able to produce a failing test from it but it's definitely a fix we
need to make.
Also moved `#link` into a constructor, because it is (and should be)
used only once.
Also made the action after an error `discard` instead of just `#unlink`
because this batch is done for and e.g. pending `settled` should
resolve, too.
We incorrectly restore effect context after an`$derived(await ...)` if
it occurs inside an async function, but only for those, no other `await`
expressions. This is buggy and wrong. We only want to restore context
_inside_ an async derived expression (including template expressions)`
so that `await a + b` works correctly.
It's possible that this will be a breaking change (albeit not
semver-violating, because `experimental`) for some people if they are
creating effects after an `await` (other than at the top level of a
component). This is regrettable, but it should never have worked in the
first place.
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>
Follow-up to #18273 (not merged yet hence no changeset here): We can run
into a null-pointer when wanting to increment/decrement inside an effect
root that is outside a the component tree. Similarly, if not using the
component logic to unset context at the right time we gotta do it
"manually" inside `save`.
---------
Co-authored-by: Rich Harris <rich.harris@vercel.com>
Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com>
## Summary
The `script_unknown_attribute` warning message used American English
(`Unrecognized`) while every other user-facing error/warning in the
compiler uses British English (`Unrecognised`):
- `options_unrecognised` error: `Unrecognised compiler option…`
- `unknown_code` warning: `is not a recognised code`
This change updates `script_unknown_attribute` to match, then
regenerates `warnings.js` and the reference documentation from the
source message file via `node scripts/process-messages`.
## Test plan
- Spelling change is isolated to the `script_unknown_attribute` warning
message
- `warnings.js` and generated docs were updated by running `node
scripts/process-messages`
- All three occurrences (source, generated JS, generated docs) are
consistent
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>
Small typo in `CONTRIBUTING.md` — "the most important and active ones
repositories" reads like a leftover from an edit. Removed the dangling
"ones".
```diff
-PRs to the most important and active ones repositories get reviewed more quickly
+PRs to the most important and active repositories get reviewed more quickly
```
Closes#18022
This updates the context docs to show the getter pattern for state that
may be reassigned, such as primitive state. The new example passes a
function through context so children read the current value instead of
capturing the initial value.
This is a docs-only clarification and does not change runtime behavior.
Validation:
- `git diff --check`
- `pnpm exec prettier --check
documentation/docs/06-runtime/02-context.md`
Full `pnpm test`/`pnpm lint` were not run because this only changes
documentation.
---------
Co-authored-by: Rich Harris <hello@rich-harris.dev>
### 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>