浏览器

Svelte

排查并修复 Svelte 与 SvelteKit 的响应式、SSR 与构建问题,并把 Svelte 4 代码迁移到 runes。

它能做什么

以症状为导向,编写、审查与调试 Svelte 与 SvelteKit 应用。覆盖 runes($state、$derived、$effect、$props)、stores、snippets、load 函数、form actions 与 adapters。适用于响应式失效(effect 循环、Map/Set 或类字段不被追踪、derived 值过期)、服务端问题(注水不匹配、SSR 报 window is not defined、请求间共享状态、adapter 构建失败)、form action 返回 403,以及 Svelte 4 到 5 迁移(export let → $props、$: → $derived、slots → snippets、on:click → onclick)。同时支持 TypeScript 类型、Vitest 与 Playwright 测试,以及产物体积调优。

什么时候用它

  • Svelte 5 runes 组件中状态变了 UI 没刷新
  • 把 Svelte 4 代码(export let、$:、slots、on:click)迁移到 Svelte 5 runes
  • SvelteKit 注水报错或 SSR 时 window is not defined
  • load 函数数据过期,form action 提交后返回 403

技能文档

User preferences and memory live in ~/Clawic/data/svelte/ (see setup.md on first use, memory-template.md for the file format). If you have data at an old location (~/svelte/ or ~/clawic/svelte/), move it to ~/Clawic/data/svelte/.

When To Use

  • Writing or reviewing Svelte components, .svelte.js state modules, or SvelteKit routes
  • Reactivity failures: UI not updating, effect loops, bindings that stop propagating, stale derived values
  • SvelteKit request-layer work: load functions, invalidation, form actions, hooks, +server.js endpoints
  • Server-only failures: hydration mismatch, window is not defined, state shared between requests, adapter build errors
  • Migrating a Svelte 4 codebase to Svelte 5 runes, or maintaining a mixed legacy/runes codebase
  • Not for Vue or Nuxt (vue, nuxt), React (react), or language-level JavaScript semantics (javascript)

Quick Reference

SituationPlay
UI does not update after a changeIs the variable $state? Destructured? A Map/Set/class instance? → Reactivity Model table, then debug.md
effect_update_depth_exceededAn effect writes state it also reads — convert to $derived, or untrack() the read (rule 2)
Value computed from other state$derived / $derived.by; never $effect + assignment (rule 2)
Deep dive on $state, $derived, $effect, $propsrunes.md
Shared state across files, context, legacy storesstores.md
Snippets, bindings, callback props, attachments, wrapping an imperative librarycomponents.md
Scoped CSS not applying, unused-selector warning, transitions, motionstyling.md
Typing props, snippets, route data, app.d.tstypescript.md
Symptom-first debugging and the error-code catalogdebug.md
Svelte 4 → 5: export let, $:, slots, on:click, new Component()migration.md
Routes, layouts, groups, param matchers, navigation, shallow routingrouting.md
load, invalidate, streaming, depends, serializationdata-loading.md
Form actions, use:enhance, validation, uploads, remote functionsforms.md
window is not defined, hydration mismatch, per-request state, env varsssr.md
Login, sessions, cookies, route guards, roles, CSRF, XSS, CSP, leaked secretssecurity.md
Bundle size, slow lists, rerender cost, preloadingperformance.md
Vitest, component tests, mocking $app/*, Playwright, svelte-checktesting.md
Adapters, prerendering, CSP, service workers, self-hostingdeployment.md
Packaging components for npm, custom elements, library API designlibrary.md
Anything elseApply Core Rules; reproduce in a single component with no props before blaming the framework

Core Rules

  1. Reactivity comes from $state, not from assignment. In runes mode a bare let count = 0 never re-renders no matter how you assign it — the compiler emits non_reactive_update. let count = $state(0) then count++ or obj.items.push(x) both work: $state on a plain object or array is a deep proxy.
  2. $derived for values, $effect for the outside world. Decision rule: if the new value is a pure function of other state → $derived; if it touches the DOM, network, storage, or a non-Svelte library → $effect. An effect that assigns state costs an extra render pass and is the direct cause of effect_update_depth_exceeded.
  3. Effects track only synchronous reads. Dependencies are collected while the effect body runs to its first await; anything read after an await, in a setTimeout, or in a .then() is invisible to the tracker and the effect will not rerun. Read your dependencies at the top, then await.
  4. Module-level state on the server belongs to every user at once. A let user = $state(null) exported from a .svelte.js module is one value per Node process, not per request — request A writes it, request B renders it. Per-request data goes in event.locals (server) and setContext/props (components).
  5. Key every {#each} whose items can move. {#each rows as row (row.id)}. Unkeyed, Svelte maps DOM to array index: delete row 0 and the input, focus, and component state of index 0 stay attached to what is now a different row. This is a correctness bug, not an optimization.
  6. Secrets and the database live behind +page.server.js. Universal +page.js runs in the browser too, so anything it imports ships. Server load output crosses the wire through devalue serialization: Date, Map, Set, BigInt, and cycles survive; class instances and functions do not (register them with the transport hook).
  7. A load function reruns only for what it read. Rerun happens when: a params or url property it accessed changes, OR an invalidate(key) matches something it depends() on or a URL it fetched, OR a parent it await parent()-ed reran, OR invalidateAll() fired. After any mutation outside use:enhance, call invalidateAll() yourself or the page keeps serving the old data.
  8. Mutations go through form actions, not fetch handlers. `` works with JavaScript disabled and on the first paint; use:enhance upgrades the same form to a fetch with no reload and reinvalidates data on success. Reach for a +server.js endpoint only for non-form clients: webhooks, third parties, file streaming.
  9. Guard browser APIs by phase, not by try/catch. $effect and onMount never run during SSR — that is the guard. Module scope and load functions run on the server, so window, document, localStorage, and IntersectionObserver there need import { browser } from '$app/environment' or a dynamic import().

Reactivity Model

What is tracked, and what silently is not:

DeclarationReactive onRule
let x = $state(0)reassignmentPrimitives: assign to update
let o = $state({a: {b: 1}})reassignment + deep mutationPlain objects/arrays become recursive proxies; o.a.b = 2 and arr.push() both work
$state.raw(bigList)reassignment onlyNo proxy: cheaper for large data you replace wholesale
new Map(), Set, Date, URL inside $statenothingNot proxied — use SvelteMap, SvelteSet, SvelteDate, SvelteURL from svelte/reactivity
Class field count = $state(0)reassignmentMethods and get accessors stay reactive; the instance itself is not a proxy
let { a, b = 1 } = $props()parent updatesDestructuring props IS reactive in runes mode — the compiler rewrites the reads
const { a } = someStateObjectnothingDestructuring state reads the value once; keep the object, or wrap in $derived
Exported let x = $state(0) from a modulenot across the importImporters get a snapshot binding — export an object, a class instance, or a getter
let x = 0 in a runes filenothingCompiler warning non_reactive_update

Passing state to a non-Svelte library (structuredClone, IndexedDB, postMessage, charting libs) hands it a Proxy; send $state.snapshot(x) instead.

Version Gates

  • svelte >=5 — runes, snippets, onclick event attributes, mount()/unmount(); $:, export let, slots, and new Component() only work in legacy mode
  • svelte >=5.3 — `` with failed snippet and onerror
  • svelte >=5.29 — attachments ({@attach fn}) supersede actions (use:fn); actions still work
  • svelte >=5.36 — experimental await inside components and $effect.pending(), behind the experimental.async compiler option
  • @sveltejs/kit >=2error() and redirect() throw internally: call them, never throw them; cookies.set requires an explicit path
  • @sveltejs/kit >=2.12$app/state (page, navigating, updated) replaces the $app/stores subscriptions; page.url, not $page.url
  • @sveltejs/kit >=2.16PageProps and LayoutProps in ./$types; below it, type $props() with { data: PageData; form: ActionData }
  • @sveltejs/kit >=2.27 — remote functions (query, form, command) in .remote.js, behind experimental.remoteFunctions

Where Code Runs

FileRunsCan hold secretsShipped to browser
+page.svelte, +layout.svelteSSR render, then clientNoYes
+page.js (universal load)Server on first load, client on navigationNoYes
+page.server.js (server load, actions)Server onlyYesNo
+server.js (endpoint)Server onlyYesNo
hooks.server.jsServer, every requestYesNo
hooks.client.jsBrowser onlyNoYes
$lib/server/**, *.server.jsServer onlyYesImport from client code = build error

Order per request: handle hook → server loads (+layout.server.js, then +page.server.js) → universal loads → render. Loads at the same level run in parallel unless one calls await parent(), which serializes it behind the parent.

Error Codes

Code or messageMeaningFirst move
state_unsafe_mutationState written while a derived or the template was computing itMove the write into an event handler or $effect
effect_update_depth_exceededAn effect writes state that it (or a chained effect) readsConvert to $derived; if the read is genuinely one-way, wrap it in untrack()
hydration_mismatchServer HTML differs from the first client renderRemove Date.now()/Math.random()/window from render; check invalid nesting (inside)
rune_outside_svelteA rune used in a .js/.ts fileRename the file to .svelte.js / .svelte.ts
lifecycle_outside_componentonMount, setContext, or getContext called after an await or inside a callbackCall synchronously during component initialization
derived_references_selfA $derived reads its own valueCompute from source state, or keep the accumulator in $state
each_key_duplicateTwo items produced the same keyKey by a unique id; never by index or by a repeated value
bind_invalid_export / binding errorbind: to a prop the child did not declare bindablelet { value = $bindable() } = $props()
ownership_invalid_mutation (dev warning)A child mutated an object owned by its parent$bindable() or a callback prop
css_unused_selector (warning)Selector matches nothing in this component's own markup:global(...), or move the rule into the child component
403 on a form POSTKit's CSRF origin check rejected a cross-origin submissionSubmit same-origin, or set csrf.checkOrigin deliberately
Cannot import $lib/server/... into client-side codeA server-only module reached a client bundleImport it in +page.server.js and pass the result through load

Configuration

User-dependent variables. Defaults apply until the user states a preference; store them in ~/Clawic/data/svelte/config.yaml.

VariableTypeDefaultEffect
syntax_moderunes | legacy | mixedrunesSelects the syntax of every generated component ($state/$props vs let/export let) and whether migration advice is offered at all
kit_projectbooltrueWhen false, drops routing, load, actions, and adapter guidance and treats the app as Svelte + Vite with client-side routing
deployment_adapterauto | node | static | vercel | cloudflare | netlifyautoDrives the deploy checklist, which env-var mechanism is valid, and whether prerender/SPA fallback is required
package_managernpm | pnpm | yarn | bunnpmSets the command syntax in install, sv add, build, and test instructions
stylingscoped-css | tailwind | unocss | css-modulesscoped-cssShapes generated markup and which scoped-CSS caveats apply
typescriptbooltrueEmits ``, typed $props(), and ./$types imports; false switches to JSDoc annotations
experimental_featuresboolfalseWhen false, remote functions, experimental.async/$effect.pending(), and attachments-over-actions are mentioned as available but never the recommended form; when true they are written by default
check_thresholderror | warningerrorThe svelte-check --threshold value in every CI recipe, and whether a warning-level finding blocks the work

Preference areas to record as the user reveals them:

  • conventions — component and route file naming, folder layout, $lib structure, barrel files
  • stack — form validation library, ORM or data client, auth approach, i18n, component library
  • progressive enhancement — whether the app must work with JavaScript disabled; decides form-action vs client-fetch mutations
  • accessibility posture — treat compiler a11y warnings as errors, or advisory
  • testing strategy — component tests in browser mode vs jsdom, and the unit/e2e split
  • risk posture — appetite for experimental and just-released APIs beyond experimental_features, upgrade cadence, and whether to propose a migration on a codebase that currently works
  • budgets — bundle-size and first-load targets, coverage floor, and any perf number the project gates on beyond check_threshold
  • output format — explanation depth (fix only vs walkthrough), full files vs diffs, comment density in generated code
  • proactivity — how eagerly to flag reactivity, boundary, and bundle issues versus answering only what was asked

Output Gates

Before emitting a component or route, verify:

  • Every mutable value declared with $state; no bare let expected to rerender?
  • Every computed value a $derived, with $effect reserved for DOM, network, storage, or third-party libraries?
  • Every {#each} over reorderable data keyed by a stable id?
  • No secret, database client, or $env/static/private import reachable from +page.svelte or +page.js?
  • Mutations expressed as a form action that still works with JavaScript disabled?
  • Browser APIs only inside $effect/onMount or behind a browser check?
  • Per-request data in event.locals or context — never a module-level variable?
  • One syntax mode per file: no export let or $: in a file that uses runes?

Traps

TrapWhy it failsDo instead
$effect used to compute derived stateExtra render pass and a loop the moment the effect reads what it writes$derived / $derived.by
Destructuring a $state object, then mutating the copyThe destructured constants captured values, not the proxyKeep the object and read o.a; $derived for a stable view
let count = $state(0) exported from a .svelte.js moduleImporters bind to the value at import timeExport a class instance or { get count() {...} }
Per-user data in a server module variableOne value per process, shared by all concurrent requestsevent.locals in hooks, setContext in components
throw redirect(...) / throw error(...) inside a tryKit 2 helpers throw internally; your catch swallows the control flowCall them after the try/catch block
Fetching your own +server.js from a server loadAn extra HTTP hop, lost types, cookie forwarding you now ownCall the function or database directly in server load
{@html userInput}XSS, and component-scoped styles do not apply to injected markupSanitize server-side; style with :global
Passing $state straight to a chart or map libraryThe library receives a Proxy and its identity checks fail$state.snapshot(value)
await early in an $effect, then reading stateReads after the await are untracked; the effect never rerunsRead every dependency synchronously first
on:click or createEventDispatcher in a runes componentLegacy syntax; the runes compiler rejects or deprecates itonclick={...} and callback props
`` in runes modeDeprecated: components are already dynamic valuesCapitalized variable: ``
bind:value to a prop the child never declared bindableBindings are opt-in in runes modelet { value = $bindable() } = $props()
document or window at module scopeModule bodies execute during SSRMove into $effect/onMount, or guard with browser
Unkeyed {#each} around inputs or stateful childrenDOM nodes are reused by indexKey by id (rule 5)

Where Experts Disagree

  • Runes vs stores for shared state. Default: a class or object with $state fields in a .svelte.js module — plain values, no $ prefix, works outside components. Stores remain the right shape for push-based external sources (sockets, geolocation, third-party subscriptions) and for anything consuming the subscribe contract; fromStore/toStore bridge the two rather than forcing a rewrite.
  • How much $effect is acceptable. The framework position is that effects are a last resort for synchronizing with systems outside Svelte; the pragmatic position accepts effects for analytics, logging, and persistence. Both agree on the hard line: an effect that assigns state which feeds back into its own dependencies is a bug, not a style choice.
  • SSR by default vs SPA. Prerendering or SSR wins for public, indexable, first-paint-sensitive pages. adapter-static with ssr = false is a legitimate choice for an internal dashboard behind auth where every view needs the client anyway — the deciding factors are SEO, first paint, and no-JS support, not fashion.
  • Validation libraries in form actions. Hand-rolled checks are fine for one to three fields. Once you need repopulating on failure, nested data, arrays, or multi-step wizards, a schema library plus a form helper stops the per-field boilerplate from drifting out of sync with the server.

More Clawic skills, get them at https://clawic.com/skills/svelte (install if the user confirms):

  • typescript — type-system depth beyond the Svelte-specific typings
  • vite — dev server, plugins, and build configuration under SvelteKit
  • playwright — end-to-end testing of the running app
  • tailwindcss — utility styling inside Svelte components
  • nodejs — running and hardening the adapter-node server

Feedback

Part of Clawic, the verified skill library. Get this skill: https://clawic.com/skills/svelte.

常见问题

这个技能能用于 Vue、Nuxt 或 React 吗?
不能。它只覆盖 Svelte 与 SvelteKit,Vue/Nuxt 和 React 明确不在范围之内,语言层面的 Java

相关技能

针对 Vue 3 响应式、组件、路由和性能问题,依据控制台报错与代码形态给出具体修复方案。

121 次安装8 星标

按规则构建、调试和审查 React 应用,覆盖组件、状态、表单、性能与 React 19 特性。

245 次安装3 星标

诊断并交付能在生产环境编译生效的 Tailwind v3/v4 标记、主题与构建配置

129 次安装5 星标

构建并调试 Next.js App Router 应用——服务端/客户端边界、缓存、Server Actions、鉴权与部署。

143 次安装4 星标

为 Node 与 TypeScript 项目设计 Prisma schema、编写类型安全查询,并解决迁移、连接池与 N+1 关联加载问题。

101 次安装2 星标

定位 Visual Studio Code 编辑器层的故障并修复:设置作用域、调试、格式化、扩展、快捷键、远程。

103 次安装3 星标