针对 Vue 3 响应式、组件、路由和性能问题,依据控制台报错与代码形态给出具体修复方案。
浏览器
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.jsstate 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.jsendpoints - 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
| Situation | Play |
|---|---|
| UI does not update after a change | Is the variable $state? Destructured? A Map/Set/class instance? → Reactivity Model table, then debug.md |
effect_update_depth_exceeded | An 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, $props | runes.md |
| Shared state across files, context, legacy stores | stores.md |
| Snippets, bindings, callback props, attachments, wrapping an imperative library | components.md |
| Scoped CSS not applying, unused-selector warning, transitions, motion | styling.md |
Typing props, snippets, route data, app.d.ts | typescript.md |
| Symptom-first debugging and the error-code catalog | debug.md |
Svelte 4 → 5: export let, $:, slots, on:click, new Component() | migration.md |
| Routes, layouts, groups, param matchers, navigation, shallow routing | routing.md |
load, invalidate, streaming, depends, serialization | data-loading.md |
Form actions, use:enhance, validation, uploads, remote functions | forms.md |
window is not defined, hydration mismatch, per-request state, env vars | ssr.md |
| Login, sessions, cookies, route guards, roles, CSRF, XSS, CSP, leaked secrets | security.md |
| Bundle size, slow lists, rerender cost, preloading | performance.md |
Vitest, component tests, mocking $app/*, Playwright, svelte-check | testing.md |
| Adapters, prerendering, CSP, service workers, self-hosting | deployment.md |
| Packaging components for npm, custom elements, library API design | library.md |
| Anything else | Apply Core Rules; reproduce in a single component with no props before blaming the framework |
Core Rules
- Reactivity comes from
$state, not from assignment. In runes mode a barelet count = 0never re-renders no matter how you assign it — the compiler emitsnon_reactive_update.let count = $state(0)thencount++orobj.items.push(x)both work:$stateon a plain object or array is a deep proxy. $derivedfor values,$effectfor 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 ofeffect_update_depth_exceeded.- Effects track only synchronous reads. Dependencies are collected while the effect body runs to its first
await; anything read after anawait, in asetTimeout, or in a.then()is invisible to the tracker and the effect will not rerun. Read your dependencies at the top, then await. - Module-level state on the server belongs to every user at once. A
let user = $state(null)exported from a.svelte.jsmodule is one value per Node process, not per request — request A writes it, request B renders it. Per-request data goes inevent.locals(server) andsetContext/props (components). - 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. - Secrets and the database live behind
+page.server.js. Universal+page.jsruns 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 thetransporthook). - A load function reruns only for what it read. Rerun happens when: a
paramsorurlproperty it accessed changes, OR aninvalidate(key)matches something itdepends()on or a URL it fetched, OR a parent itawait parent()-ed reran, ORinvalidateAll()fired. After any mutation outsideuse:enhance, callinvalidateAll()yourself or the page keeps serving the old data. - Mutations go through form actions, not fetch handlers. `` works with JavaScript disabled and on the first paint;
use:enhanceupgrades the same form to a fetch with no reload and reinvalidates data on success. Reach for a+server.jsendpoint only for non-form clients: webhooks, third parties, file streaming. - Guard browser APIs by phase, not by try/catch.
$effectandonMountnever run during SSR — that is the guard. Module scope and load functions run on the server, sowindow,document,localStorage, andIntersectionObserverthere needimport { browser } from '$app/environment'or a dynamicimport().
Reactivity Model
What is tracked, and what silently is not:
| Declaration | Reactive on | Rule |
|---|---|---|
let x = $state(0) | reassignment | Primitives: assign to update |
let o = $state({a: {b: 1}}) | reassignment + deep mutation | Plain objects/arrays become recursive proxies; o.a.b = 2 and arr.push() both work |
$state.raw(bigList) | reassignment only | No proxy: cheaper for large data you replace wholesale |
new Map(), Set, Date, URL inside $state | nothing | Not proxied — use SvelteMap, SvelteSet, SvelteDate, SvelteURL from svelte/reactivity |
Class field count = $state(0) | reassignment | Methods and get accessors stay reactive; the instance itself is not a proxy |
let { a, b = 1 } = $props() | parent updates | Destructuring props IS reactive in runes mode — the compiler rewrites the reads |
const { a } = someStateObject | nothing | Destructuring state reads the value once; keep the object, or wrap in $derived |
Exported let x = $state(0) from a module | not across the import | Importers get a snapshot binding — export an object, a class instance, or a getter |
let x = 0 in a runes file | nothing | Compiler 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,onclickevent attributes,mount()/unmount();$:,export let, slots, andnew Component()only work in legacy modesvelte >=5.3— `` withfailedsnippet andonerrorsvelte >=5.29— attachments ({@attach fn}) supersede actions (use:fn); actions still worksvelte >=5.36— experimentalawaitinside components and$effect.pending(), behind theexperimental.asynccompiler option@sveltejs/kit >=2—error()andredirect()throw internally: call them, neverthrowthem;cookies.setrequires an explicitpath@sveltejs/kit >=2.12—$app/state(page,navigating,updated) replaces the$app/storessubscriptions;page.url, not$page.url@sveltejs/kit >=2.16—PagePropsandLayoutPropsin./$types; below it, type$props()with{ data: PageData; form: ActionData }@sveltejs/kit >=2.27— remote functions (query,form,command) in.remote.js, behindexperimental.remoteFunctions
Where Code Runs
| File | Runs | Can hold secrets | Shipped to browser |
|---|---|---|---|
+page.svelte, +layout.svelte | SSR render, then client | No | Yes |
+page.js (universal load) | Server on first load, client on navigation | No | Yes |
+page.server.js (server load, actions) | Server only | Yes | No |
+server.js (endpoint) | Server only | Yes | No |
hooks.server.js | Server, every request | Yes | No |
hooks.client.js | Browser only | No | Yes |
$lib/server/**, *.server.js | Server only | Yes | Import 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 message | Meaning | First move |
|---|---|---|
state_unsafe_mutation | State written while a derived or the template was computing it | Move the write into an event handler or $effect |
effect_update_depth_exceeded | An effect writes state that it (or a chained effect) reads | Convert to $derived; if the read is genuinely one-way, wrap it in untrack() |
hydration_mismatch | Server HTML differs from the first client render | Remove Date.now()/Math.random()/window from render; check invalid nesting (inside) |
rune_outside_svelte | A rune used in a .js/.ts file | Rename the file to .svelte.js / .svelte.ts |
lifecycle_outside_component | onMount, setContext, or getContext called after an await or inside a callback | Call synchronously during component initialization |
derived_references_self | A $derived reads its own value | Compute from source state, or keep the accumulator in $state |
each_key_duplicate | Two items produced the same key | Key by a unique id; never by index or by a repeated value |
bind_invalid_export / binding error | bind: to a prop the child did not declare bindable | let { 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 POST | Kit's CSRF origin check rejected a cross-origin submission | Submit same-origin, or set csrf.checkOrigin deliberately |
Cannot import $lib/server/... into client-side code | A server-only module reached a client bundle | Import 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.
| Variable | Type | Default | Effect |
|---|---|---|---|
| syntax_mode | runes | legacy | mixed | runes | Selects the syntax of every generated component ($state/$props vs let/export let) and whether migration advice is offered at all |
| kit_project | bool | true | When false, drops routing, load, actions, and adapter guidance and treats the app as Svelte + Vite with client-side routing |
| deployment_adapter | auto | node | static | vercel | cloudflare | netlify | auto | Drives the deploy checklist, which env-var mechanism is valid, and whether prerender/SPA fallback is required |
| package_manager | npm | pnpm | yarn | bun | npm | Sets the command syntax in install, sv add, build, and test instructions |
| styling | scoped-css | tailwind | unocss | css-modules | scoped-css | Shapes generated markup and which scoped-CSS caveats apply |
| typescript | bool | true | Emits ``, typed $props(), and ./$types imports; false switches to JSDoc annotations |
| experimental_features | bool | false | When 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_threshold | error | warning | error | The 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,
$libstructure, 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 bareletexpected to rerender? - Every computed value a
$derived, with$effectreserved 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/privateimport reachable from+page.svelteor+page.js? - Mutations expressed as a form action that still works with JavaScript disabled?
- Browser APIs only inside
$effect/onMountor behind abrowsercheck? - Per-request data in
event.localsor context — never a module-level variable? - One syntax mode per file: no
export letor$:in a file that uses runes?
Traps
| Trap | Why it fails | Do instead |
|---|---|---|
$effect used to compute derived state | Extra render pass and a loop the moment the effect reads what it writes | $derived / $derived.by |
Destructuring a $state object, then mutating the copy | The destructured constants captured values, not the proxy | Keep the object and read o.a; $derived for a stable view |
let count = $state(0) exported from a .svelte.js module | Importers bind to the value at import time | Export a class instance or { get count() {...} } |
| Per-user data in a server module variable | One value per process, shared by all concurrent requests | event.locals in hooks, setContext in components |
throw redirect(...) / throw error(...) inside a try | Kit 2 helpers throw internally; your catch swallows the control flow | Call them after the try/catch block |
Fetching your own +server.js from a server load | An extra HTTP hop, lost types, cookie forwarding you now own | Call the function or database directly in server load |
{@html userInput} | XSS, and component-scoped styles do not apply to injected markup | Sanitize server-side; style with :global |
Passing $state straight to a chart or map library | The library receives a Proxy and its identity checks fail | $state.snapshot(value) |
await early in an $effect, then reading state | Reads after the await are untracked; the effect never reruns | Read every dependency synchronously first |
on:click or createEventDispatcher in a runes component | Legacy syntax; the runes compiler rejects or deprecates it | onclick={...} and callback props |
| `` in runes mode | Deprecated: components are already dynamic values | Capitalized variable: `` |
bind:value to a prop the child never declared bindable | Bindings are opt-in in runes mode | let { value = $bindable() } = $props() |
document or window at module scope | Module bodies execute during SSR | Move into $effect/onMount, or guard with browser |
Unkeyed {#each} around inputs or stateful children | DOM nodes are reused by index | Key by id (rule 5) |
Where Experts Disagree
- Runes vs stores for shared state. Default: a class or object with
$statefields in a.svelte.jsmodule — 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 thesubscribecontract;fromStore/toStorebridge the two rather than forcing a rewrite. - How much
$effectis 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-staticwithssr = falseis 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.
Related Skills
More Clawic skills, get them at https://clawic.com/skills/svelte (install if the user confirms):
typescript— type-system depth beyond the Svelte-specific typingsvite— dev server, plugins, and build configuration under SvelteKitplaywright— end-to-end testing of the running apptailwindcss— utility styling inside Svelte componentsnodejs— running and hardening theadapter-nodeserver
Feedback
- If useful, star it: https://clawic.com/skills/svelte
- Latest version: https://clawic.com/skills/svelte
Part of Clawic, the verified skill library. Get this skill: https://clawic.com/skills/svelte.
常见问题
- 这个技能能用于 Vue、Nuxt 或 React 吗?
- 不能。它只覆盖 Svelte 与 SvelteKit,Vue/Nuxt 和 React 明确不在范围之内,语言层面的 Java
相关技能
按规则构建、调试和审查 React 应用,覆盖组件、状态、表单、性能与 React 19 特性。
诊断并交付能在生产环境编译生效的 Tailwind v3/v4 标记、主题与构建配置
构建并调试 Next.js App Router 应用——服务端/客户端边界、缓存、Server Actions、鉴权与部署。
为 Node 与 TypeScript 项目设计 Prisma schema、编写类型安全查询,并解决迁移、连接池与 N+1 关联加载问题。
定位 Visual Studio Code 编辑器层的故障并修复:设置作用域、调试、格式化、扩展、快捷键、远程。