From 3fc4bc6774d8cb298e0b9396e82432e37d771ab1 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 27 Feb 2026 07:22:50 -0500 Subject: [PATCH 1/9] chore: remove unused is_flushing variable (#17820) as of a few PRs ago this variable is unused --- packages/svelte/src/internal/client/reactivity/batch.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 263674d35f..4a4864581d 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -66,7 +66,6 @@ let queued_root_effects = []; /** @type {Effect | null} */ let last_scheduled_effect = null; -let is_flushing = false; export let is_flushing_sync = false; /** @@ -589,8 +588,6 @@ export function flushSync(fn) { } function flush_effects() { - is_flushing = true; - var source_stacks = DEV ? new Set() : null; try { @@ -639,7 +636,6 @@ function flush_effects() { } finally { queued_root_effects = []; - is_flushing = false; last_scheduled_effect = null; collected_effects = null; From 18db0cab86cf15198be059c60c044b66512e573e Mon Sep 17 00:00:00 2001 From: Mathias Picker <48158184+MathiasWP@users.noreply.github.com> Date: Fri, 27 Feb 2026 20:26:53 +0100 Subject: [PATCH 2/9] fix: SvelteMap incorrectly handles keys with `undefined` values (#17826) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary `SvelteMap` had two bugs related to how it checked for key existence internally: ### 1. `has()` and `get()` returned wrong results for keys with `undefined` values Both methods used `super.get(key) !== undefined` to determine if a key existed before creating a per-key reactive source. This fails for keys whose value is legitimately `undefined`, causing: - `has(key)` to return `false` for existing keys with `undefined` values - `get(key)` to skip creating a per-key source and fall back to tracking `version`, resulting in over-notification **Fix:** Replace `super.get(key) !== undefined` with `super.has(key)` in both `has()` and `get()`, matching the pattern already used in `SvelteSet`. ### 2. `delete()` skipped reactive updates when a key had no per-key source The `size` and `version` reactive updates were inside the `if (s !== undefined)` block, meaning they only fired when a per-key source existed (i.e., someone had previously called `has()` or `get()` on that specific key). If a key was added via the constructor or `set()` but never individually read, deleting it would not trigger reactive updates for effects depending on `size` or iterators. **Fix:** Move `set(this.#size, super.size)` and `increment(this.#version)` to a separate `if (res)` block so they fire whenever a key is actually deleted, regardless of whether a per-key source existed. ### Before fix ```js const map = new SvelteMap([['foo', undefined]]); map.has('foo'); // false (should be true) map.get('foo'); // undefined but tracks version instead of per-key source ``` ### After fix ```js const map = new SvelteMap([['foo', undefined]]); map.has('foo'); // true map.get('foo'); // undefined with correct per-key tracking ``` ## Test plan Tests are in `packages/svelte/src/reactivity/map.test.ts`: - `map.has()` returns `true` for constructor-initialized keys with `undefined` values - `map.get()` returns `undefined` with proper per-key reactive tracking - `map.delete()` triggers `has()`/`get()` reactivity for undefined-valued keys - `map.set(key, undefined)` followed by `has()`/`get()` works correctly - `map.delete()` triggers `size` reactivity for keys that were never individually read (no per-key source) - All existing tests pass unchanged 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 --- .changeset/fix-svelte-map-undefined.md | 5 ++ packages/svelte/src/reactivity/map.js | 11 ++-- packages/svelte/src/reactivity/map.test.ts | 69 ++++++++++++++++++++++ 3 files changed, 80 insertions(+), 5 deletions(-) create mode 100644 .changeset/fix-svelte-map-undefined.md diff --git a/.changeset/fix-svelte-map-undefined.md b/.changeset/fix-svelte-map-undefined.md new file mode 100644 index 0000000000..fa981c46f8 --- /dev/null +++ b/.changeset/fix-svelte-map-undefined.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: `SvelteMap` incorrectly handles keys with `undefined` values diff --git a/packages/svelte/src/reactivity/map.js b/packages/svelte/src/reactivity/map.js index 014b5e7c7c..48d06a05a7 100644 --- a/packages/svelte/src/reactivity/map.js +++ b/packages/svelte/src/reactivity/map.js @@ -98,8 +98,7 @@ export class SvelteMap extends Map { var s = sources.get(key); if (s === undefined) { - var ret = super.get(key); - if (ret !== undefined) { + if (super.has(key)) { s = this.#source(0); if (DEV) { @@ -134,8 +133,7 @@ export class SvelteMap extends Map { var s = sources.get(key); if (s === undefined) { - var ret = super.get(key); - if (ret !== undefined) { + if (super.has(key)) { s = this.#source(0); if (DEV) { @@ -202,8 +200,11 @@ export class SvelteMap extends Map { if (s !== undefined) { sources.delete(key); - set(this.#size, super.size); set(s, -1); + } + + if (res) { + set(this.#size, super.size); increment(this.#version); } diff --git a/packages/svelte/src/reactivity/map.test.ts b/packages/svelte/src/reactivity/map.test.ts index 2f9f064b42..8bb6f72f7b 100644 --- a/packages/svelte/src/reactivity/map.test.ts +++ b/packages/svelte/src/reactivity/map.test.ts @@ -207,6 +207,75 @@ test('map handling of undefined values', () => { cleanup(); }); +test('map.has() and map.get() with undefined values', () => { + const map = new SvelteMap([['foo', undefined]]); + + const log: any = []; + + const cleanup = effect_root(() => { + render_effect(() => { + log.push('has', map.has('foo')); + }); + + render_effect(() => { + log.push('get', map.get('foo')); + }); + + flushSync(() => { + map.delete('foo'); + }); + + flushSync(() => { + map.set('bar', undefined); + }); + }); + + assert.deepEqual(log, [ + 'has', + true, + 'get', + undefined, + 'has', + false, + 'get', + undefined, + // set('bar') bumps version, causing has('foo')/get('foo') effects to re-run + 'has', + false, + 'get', + undefined + ]); + + assert.equal(map.has('bar'), true); + assert.equal(map.get('bar'), undefined); + + cleanup(); +}); + +test('map.delete() triggers size reactivity for keys without per-key sources', () => { + const map = new SvelteMap([ + [1, 'a'], + [2, 'b'] + ]); + + const log: any = []; + + const cleanup = effect_root(() => { + render_effect(() => { + log.push(map.size); + }); + + // delete key 2 which was never individually read (no per-key source) + flushSync(() => { + map.delete(2); + }); + }); + + assert.deepEqual(log, [2, 1]); + + cleanup(); +}); + test('not invoking reactivity when value is not in the map after changes', () => { const map = new SvelteMap([[1, 1]]); From a10cf95ca732dae624401489973c1814a3e0fde7 Mon Sep 17 00:00:00 2001 From: Tee Ming Date: Sat, 28 Feb 2026 03:49:02 +0800 Subject: [PATCH 3/9] chore: add funding manifest URL (#17827) Related to https://github.com/sveltejs/svelte.dev/pull/1632 This will allow us to apply for a grant from floss.fund ### 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 - [ ] Prefix your PR title with `feat:`, `fix:`, `chore:`, or `docs:`. - [ ] This message body should clearly illustrate what problems it solves. - [ ] Ideally, include a test that fails without this PR but passes with it. - [ ] If this PR changes code within `packages/svelte/src`, add a changeset (`npx changeset`). ### Tests and linting - [ ] Run the tests with `pnpm test` and lint the project with `pnpm lint` --- .well-known/funding-manifest-urls | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .well-known/funding-manifest-urls diff --git a/.well-known/funding-manifest-urls b/.well-known/funding-manifest-urls new file mode 100644 index 0000000000..b8ccc27b78 --- /dev/null +++ b/.well-known/funding-manifest-urls @@ -0,0 +1,2 @@ +https://svelte.dev/funding.json + From e3d277b000dcca2bcb391b1cb92899484d7173e7 Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Fri, 27 Feb 2026 21:01:13 +0100 Subject: [PATCH 4/9] fix: visit synthetic value node during ssr (#17824) Closes #17821 ### 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` --- .changeset/itchy-carpets-watch.md | 5 +++++ .../3-transform/server/visitors/RegularElement.js | 2 +- .../_expected.html | 1 + .../select-option-store-implicit-value/main.svelte | 11 +++++++++++ .../select-option-store-text-content/_expected.html | 1 + .../select-option-store-text-content/main.svelte | 13 +++++++++++++ 6 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 .changeset/itchy-carpets-watch.md create mode 100644 packages/svelte/tests/server-side-rendering/samples/select-option-store-implicit-value/_expected.html create mode 100644 packages/svelte/tests/server-side-rendering/samples/select-option-store-implicit-value/main.svelte create mode 100644 packages/svelte/tests/server-side-rendering/samples/select-option-store-text-content/_expected.html create mode 100644 packages/svelte/tests/server-side-rendering/samples/select-option-store-text-content/main.svelte diff --git a/.changeset/itchy-carpets-watch.md b/.changeset/itchy-carpets-watch.md new file mode 100644 index 0000000000..2bafdae5ea --- /dev/null +++ b/.changeset/itchy-carpets-watch.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: visit synthetic value node during ssr diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/RegularElement.js index e653f545af..42d9bcb667 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/RegularElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/RegularElement.js @@ -126,7 +126,7 @@ export function RegularElement(node, context) { if (node.metadata.synthetic_value_node) { body = optimiser.transform( - node.metadata.synthetic_value_node.expression, + /** @type {Expression} */ (context.visit(node.metadata.synthetic_value_node.expression)), node.metadata.synthetic_value_node.metadata.expression ); } else { diff --git a/packages/svelte/tests/server-side-rendering/samples/select-option-store-implicit-value/_expected.html b/packages/svelte/tests/server-side-rendering/samples/select-option-store-implicit-value/_expected.html new file mode 100644 index 0000000000..0a0a4351f1 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/select-option-store-implicit-value/_expected.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/svelte/tests/server-side-rendering/samples/select-option-store-implicit-value/main.svelte b/packages/svelte/tests/server-side-rendering/samples/select-option-store-implicit-value/main.svelte new file mode 100644 index 0000000000..5d133d5bf3 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/select-option-store-implicit-value/main.svelte @@ -0,0 +1,11 @@ + + + diff --git a/packages/svelte/tests/server-side-rendering/samples/select-option-store-text-content/_expected.html b/packages/svelte/tests/server-side-rendering/samples/select-option-store-text-content/_expected.html new file mode 100644 index 0000000000..daf2176928 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/select-option-store-text-content/_expected.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/svelte/tests/server-side-rendering/samples/select-option-store-text-content/main.svelte b/packages/svelte/tests/server-side-rendering/samples/select-option-store-text-content/main.svelte new file mode 100644 index 0000000000..27055caad6 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/select-option-store-text-content/main.svelte @@ -0,0 +1,13 @@ + + + From b6faa2a905804f77a6c486729f9ce8325c273273 Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Fri, 27 Feb 2026 21:08:42 +0100 Subject: [PATCH 5/9] fix: always case insensitive event handlers during ssr (#17822) Fixes events not being stripped on svg, mathml and custom elements. ### 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` --------- Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> --- .changeset/proud-seas-study.md | 5 +++++ packages/svelte/src/internal/server/index.js | 7 +++---- .../attribute-strip-svg-mathml-ce/_expected.html | 1 + .../attribute-strip-svg-mathml-ce/main.svelte | 16 ++++++++++++++++ 4 files changed, 25 insertions(+), 4 deletions(-) create mode 100644 .changeset/proud-seas-study.md create mode 100644 packages/svelte/tests/server-side-rendering/samples/attribute-strip-svg-mathml-ce/_expected.html create mode 100644 packages/svelte/tests/server-side-rendering/samples/attribute-strip-svg-mathml-ce/main.svelte diff --git a/.changeset/proud-seas-study.md b/.changeset/proud-seas-study.md new file mode 100644 index 0000000000..ec12b7232a --- /dev/null +++ b/.changeset/proud-seas-study.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: always case insensitive event handlers during ssr diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index 70f1f6dab8..34d0133a31 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -154,13 +154,12 @@ export function attributes(attrs, css_hash, classes, styles, flags = 0) { if (INVALID_ATTR_NAME_CHAR_REGEX.test(name)) continue; var value = attrs[name]; + var lower = name.toLowerCase(); - if (lowercase) { - name = name.toLowerCase(); - } + if (lowercase) name = lower; // omit event handler attributes - if (name.length > 2 && name.startsWith('on')) continue; + if (lower.length > 2 && lower.startsWith('on')) continue; if (is_input) { if (name === 'defaultvalue' || name === 'defaultchecked') { diff --git a/packages/svelte/tests/server-side-rendering/samples/attribute-strip-svg-mathml-ce/_expected.html b/packages/svelte/tests/server-side-rendering/samples/attribute-strip-svg-mathml-ce/_expected.html new file mode 100644 index 0000000000..e737f44c56 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/attribute-strip-svg-mathml-ce/_expected.html @@ -0,0 +1 @@ + x \ No newline at end of file diff --git a/packages/svelte/tests/server-side-rendering/samples/attribute-strip-svg-mathml-ce/main.svelte b/packages/svelte/tests/server-side-rendering/samples/attribute-strip-svg-mathml-ce/main.svelte new file mode 100644 index 0000000000..df15de54de --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/attribute-strip-svg-mathml-ce/main.svelte @@ -0,0 +1,16 @@ + + + + + + + + x + + + \ No newline at end of file From 1043f79d1e46f3e0194e18e370244d33e6d9de19 Mon Sep 17 00:00:00 2001 From: Mathias Picker <48158184+MathiasWP@users.noreply.github.com> Date: Fri, 27 Feb 2026 21:14:20 +0100 Subject: [PATCH 6/9] perf: optimize compiler analysis phase (#17823) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Two optimizations to the compiler's analysis phase: - **Cache `ignore_stack` snapshots instead of `structuredClone` on every node.** The universal `_` visitor in the analysis walk runs on every AST node and calls `structuredClone(ignore_stack)` each time. In practice, `svelte-ignore` comments are rare (0–5 per component), so 99%+ of nodes deep-clone an unchanged stack. This adds a copy-on-write cache that only re-creates the snapshot when `push_ignore`/`pop_ignore` actually change the stack. - **Walk the CSS stylesheet once instead of once per element.** `prune()` was called in a loop for each element, each time doing a full `walk()` of the stylesheet AST. This restructures the loop so the stylesheet is walked once, and the element iteration happens inside the `ComplexSelector` visitor. ## Benchmarks Compiled each component 500 times (after 50 warmup iterations), measuring average time per `compile()` call: | Component | Before | After | Speedup | |---|---|---|---| | `has` (80+ CSS selectors, 12 elements) | 3.405 ms | 2.680 ms | **21% faster** | | `siblings-combinator-each-nested` (65 CSS rules, 15 elements) | 2.034 ms | 1.575 ms | **23% faster** | | synthetic (100 CSS rules, 50 elements) | 10.099 ms | 4.564 ms | **55% faster** | The CSS pruning optimization scales with `elements × CSS rules` — the more elements a component has, the bigger the win since we go from N stylesheet walks down to 1. The `structuredClone` fix helps every component regardless of CSS, eliminating ~500–2000 deep clones per compile (one per AST node) and replacing them with 0–5 (one per `svelte-ignore` comment). For typical real-world components with 10–20 elements and some CSS, expect roughly **20–30% faster compilation** in the analysis phase. ## Test plan - [x] Full test suite passes (7329 tests, 0 failures) - [x] CSS pruning tests pass (selector matching, scoping, unused rule detection) - [x] `svelte-ignore` behavior unchanged (snapshot is consumed read-only via `.has()`/`.some()`) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 --- .changeset/tidy-brooms-train.md | 5 ++++ .../phases/2-analyze/css/css-prune.js | 26 ++++++++++--------- .../src/compiler/phases/2-analyze/index.js | 8 +++--- packages/svelte/src/compiler/state.js | 23 ++++++++++++++++ 4 files changed, 45 insertions(+), 17 deletions(-) create mode 100644 .changeset/tidy-brooms-train.md diff --git a/.changeset/tidy-brooms-train.md b/.changeset/tidy-brooms-train.md new file mode 100644 index 0000000000..4f680bd33f --- /dev/null +++ b/.changeset/tidy-brooms-train.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +perf: optimize compiler analysis phase diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js index 7e05d2e7d3..24da276ed5 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js @@ -125,9 +125,9 @@ const seen = new Set(); /** * * @param {Compiler.AST.CSS.StyleSheet} stylesheet - * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element + * @param {Iterable} elements */ -export function prune(stylesheet, element) { +export function prune(stylesheet, elements) { walk(/** @type {Compiler.AST.CSS.Node} */ (stylesheet), null, { Rule(node, context) { if (node.metadata.is_global_block) { @@ -139,17 +139,19 @@ export function prune(stylesheet, element) { ComplexSelector(node) { const selectors = get_relative_selectors(node); - seen.clear(); + for (const element of elements) { + seen.clear(); - if ( - apply_selector( - selectors, - /** @type {Compiler.AST.CSS.Rule} */ (node.metadata.rule), - element, - BACKWARD - ) - ) { - node.metadata.used = true; + if ( + apply_selector( + selectors, + /** @type {Compiler.AST.CSS.Rule} */ (node.metadata.rule), + element, + BACKWARD + ) + ) { + node.metadata.used = true; + } } // note: we don't call context.next() here, we only recurse into diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 969af842cc..fbd2e1cb8a 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -21,7 +21,7 @@ import { prune } from './css/css-prune.js'; import { hash, is_rune } from '../../../utils.js'; import { warn_unused } from './css/css-warn.js'; import { extract_svelte_ignore } from '../../utils/extract_svelte_ignore.js'; -import { ignore_map, ignore_stack, pop_ignore, push_ignore } from '../../state.js'; +import { ignore_map, get_ignore_snapshot, pop_ignore, push_ignore } from '../../state.js'; import { ArrowFunctionExpression } from './visitors/ArrowFunctionExpression.js'; import { AssignmentExpression } from './visitors/AssignmentExpression.js'; import { AnimateDirective } from './visitors/AnimateDirective.js'; @@ -134,7 +134,7 @@ const visitors = { push_ignore(ignores); } - ignore_map.set(node, structuredClone(ignore_stack)); + ignore_map.set(node, get_ignore_snapshot()); const scope = state.scopes.get(node); next(scope !== undefined && scope !== state.scope ? { ...state, scope } : state); @@ -856,9 +856,7 @@ export function analyze_component(root, source, options) { analyze_css(analysis.css.ast, analysis); // mark nodes as scoped/unused/empty etc - for (const node of analysis.elements) { - prune(analysis.css.ast, node); - } + prune(analysis.css.ast, analysis.elements); const { comment } = analysis.css.ast.content; const should_ignore_unused = diff --git a/packages/svelte/src/compiler/state.js b/packages/svelte/src/compiler/state.js index 37aeafe595..c380143f4f 100644 --- a/packages/svelte/src/compiler/state.js +++ b/packages/svelte/src/compiler/state.js @@ -82,16 +82,38 @@ export let ignore_stack = []; */ export let ignore_map = new Map(); +/** + * Cached snapshot of the ignore_stack. Only re-created when the stack changes + * (i.e. when push_ignore or pop_ignore is called), avoiding a structuredClone + * on every node visit during analysis. + * @type {Set[] | null} + */ +let cached_ignore_snapshot = null; + +/** + * Returns a snapshot of the current ignore_stack, reusing a cached copy + * when the stack hasn't changed since the last call. + * @returns {Set[]} + */ +export function get_ignore_snapshot() { + if (cached_ignore_snapshot === null) { + cached_ignore_snapshot = ignore_stack.map((s) => new Set(s)); + } + return cached_ignore_snapshot; +} + /** * @param {string[]} ignores */ export function push_ignore(ignores) { const next = new Set([...(ignore_stack.at(-1) || []), ...ignores]); ignore_stack.push(next); + cached_ignore_snapshot = null; } export function pop_ignore() { ignore_stack.pop(); + cached_ignore_snapshot = null; } /** @@ -141,4 +163,5 @@ export function adjust(state) { ignore_stack = []; ignore_map.clear(); + cached_ignore_snapshot = null; } From 6e9b2a6fd85e01a12aaca4b883eea4d15ae14d36 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 27 Feb 2026 15:35:19 -0500 Subject: [PATCH 7/9] docs: best practices (#17804) Based on #17727 but with a few changes: - smaller (fewer tokens) - removed some bits that are really just summarising existing docs - added an explicit 'don't use these legacy features' section - shuffled some things around a bit --------- Co-authored-by: paoloricciuti Co-authored-by: Federico Varano <50919335+fvarano@users.noreply.github.com> Co-authored-by: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> --- .../docs/07-misc/01-best-practices.md | 184 ++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 documentation/docs/07-misc/01-best-practices.md diff --git a/documentation/docs/07-misc/01-best-practices.md b/documentation/docs/07-misc/01-best-practices.md new file mode 100644 index 0000000000..ac275e4d9a --- /dev/null +++ b/documentation/docs/07-misc/01-best-practices.md @@ -0,0 +1,184 @@ +--- +title: Best practices +name: svelte-core-bestpractices +description: Guidance on writing fast, robust, modern Svelte code. Load this skill whenever in a Svelte project and asked to write/edit or analyze a Svelte component or module. Covers reactivity, event handling, styling, integration with libraries and more. +--- + + +This document outlines some best practices that will help you write fast, robust Svelte apps. It is also available as a `svelte-core-bestpractices` skill for your agents. + + +## `$state` + +Only use the `$state` rune for variables that should be _reactive_ — in other words, variables that cause an `$effect`, `$derived` or template expression to update. Everything else can be a normal variable. + +Objects and arrays (`$state({...})` or `$state([...])`) are made deeply reactive, meaning mutation will trigger updates. This has a trade-off: in exchange for fine-grained reactivity, the objects must be proxied, which has performance overhead. In cases where you're dealing with large objects that are only ever reassigned (rather than mutated), use `$state.raw` instead. This is often the case with API responses, for example. + +## `$derived` + +To compute something from state, use `$derived` rather than `$effect`: + +```js +// @errors: 2451 +let num = 0; +// ---cut--- +// do this +let square = $derived(num * num); + +// don't do this +let square; + +$effect(() => { + square = num * num; +}); +``` + +> [!NOTE] `$derived` is given an expression, _not_ a function. If you need to use a function (because the expression is complex, for example) use `$derived.by`. + +Deriveds are writable — you can assign to them, just like `$state`, except that they will re-evaluate when their expression changes. + +If the derived expression is an object or array, it will be returned as-is — it is _not_ made deeply reactive. You can, however, use `$state` inside `$derived.by` in the rare cases that you need this. + +## `$effect` + +Effects are an escape hatch and should mostly be avoided. In particular, avoid updating state inside effects. + +- If you need to sync state to an external library such as D3, it is often neater to use [`{@attach ...}`](@attach) +- If you need to run some code in response to user interaction, put the code directly in an event handler or use a [function binding](bind#Function-bindings) as appropriate +- If you need to log values for debugging purposes, use [`$inspect`]($inspect) +- If you need to observe something external to Svelte, use [`createSubscriber`](svelte-reactivity#createSubscriber) + +Never wrap the contents of an effect in `if (browser) {...}` or similar — effects do not run on the server. + +## `$props` + +Treat props as though they will change. For example, values that depend on props should usually use `$derived`: + +```js +// @errors: 2451 +let { type } = $props(); + +// do this +let color = $derived(type === 'danger' ? 'red' : 'green'); + +// don't do this — `color` will not update if `type` changes +let color = type === 'danger' ? 'red' : 'green'; +``` + +## `$inspect.trace` + +`$inspect.trace` is a debugging tool for reactivity. If something is not updating properly or running more than it should you can add `$inspect.trace(label)` as the first line of an `$effect` or `$derived.by` (or any function they call) to trace their dependencies and discover which one triggered an update. + +## Events + +Any element attribute starting with `on` is treated as an event listener: + +```svelte + + + + + + + +``` + +If you need to attach listeners to `window` or `document` you can use `` and ``: + +```svelte + + +``` + +Avoid using `onMount` or `$effect` for this. + +## Snippets + +[Snippets](snippet) are a way to define reusable chunks of markup that can be instantiated with the [`{@render ...}`](@render) tag, or passed to components as props. They must be declared within the template. + +```svelte +{#snippet greeting(name)} +

hello {name}!

+{/snippet} + +{@render greeting('world')} +``` + +> [!NOTE] Snippets declared at the top level of a component (i.e. not inside elements or blocks) can be referenced inside `