Yesterday the Svelte team shipped SvelteKit 3.0, and by this morning the announcement was sitting on the Hacker News front page with over 350 points and more than 150 comments. That is a strong signal. When a framework whose previous major version came out in 2022 jumps a whole number, thousands of production apps have a migration in their future, and the comment section fills up with people comparing notes on what broke.
I write TypeScript and Svelte as part of my daily toolkit, so this release matters to me directly. Full disclosure though: I have not yet run this migration on a production app, so this is not a war story. It is the opposite. I read the release announcement, the release candidate post, and the full official migration guide end to end, and turned them into the checklist I will follow when I upgrade. Every claim below links to the official source, so you can verify anything yourself.
Here is everything that changes, in roughly the order you will hit it.
Step 0: Check your floor versions first
Before touching any code, SvelteKit 3 raises the minimum versions of everything underneath it. From the migration guide, you need at minimum:
- Node v22.17 or newer
- TypeScript v6 or newer
- Svelte v5.56.4 or newer
- Vite v8.0.12, the first Vite 8 release bundling stable Rolldown v1
- @sveltejs/vite-plugin-svelte v7
Two of these deserve attention. The TypeScript 6 requirement will catch teams that pinned TS 5 for plugin compatibility. And SvelteKit 3 requires Svelte 5, which is the entire reason the error handling story improves later in this list.
The guide also recommends upgrading to the most recent 2.x release before jumping to 3.0, because 2.x emits targeted deprecation warnings that map one to one onto the breaking changes. Skipping that step means losing your best migration assistant.
The config file is dead. Long live vite.config.ts
The biggest structural change: svelte.config.js is no longer supported at all. All configuration now lives in the Vite plugin options inside vite.config.ts.
The release candidate post explains the reasoning, and it is sound. The Vite plugin previously had to resolve your config through an asynchronous process that could not start until the entire Vite config was resolved, because tools like Vitest can run with a working directory that is not the project root. Moving config into the plugin call gives the plugin immediate access. And honestly, the simpler question: why were we maintaining two config files for one project?
In practice, config.kit.* options become top-level plugin options:
import { defineConfig } from 'vite';
import { sveltekit } from '@sveltejs/kit/vite';
import adapter from '@sveltejs/adapter-auto';
export default defineConfig({
plugins: [
sveltekit({
compilerOptions: { experimental: { async: true } },
adapter: adapter()
})
]
});
A few options died entirely and should just be deleted, per the migration guide:
- preloadStrategy is removed. modulepreload is now supported everywhere, so SvelteKit always uses it.
-
prerender.origin is replaced by a top-level
paths.originoption. -
csrf.checkOrigin is replaced by
csrf.trustedOrigins.
The $lib alias becomes #lib, with a sharp edge
This is the change that touches every file in a real codebase. SvelteKit no longer generates the $lib alias. Instead you declare a #lib alias in the imports field of your package.json, using Node's built-in subpath imports, which Vite and TypeScript resolve natively:
{
"imports": {
"#lib": "./src/lib/index.js",
"#lib/*": "./src/lib/*"
}
}
Then replace $lib with #lib across your codebase. The migration script handles most of this, but read the gotcha before you trust it: Node and TypeScript require subpath imports to be unambiguous. Importing #lib/foo fails. You must write #lib/foo.ts or #lib/foo/index.ts, with the extension. If your imports were already fully explicit, this is painless. If you relied on extensionless resolution, expect a pass through your whole src/lib tree.
TypeScript config gets simpler
In SvelteKit 2, your tsconfig.json extended the generated ./.svelte-kit/tsconfig.json. In SvelteKit 3 you extend $app/tsconfig instead, a generated file in node_modules that now carries more recommended compiler options. The RC post notes you can likely delete most of your own compilerOptions unless you have unusual needs. You should explicitly set your include and exclude arrays, and exclude should cover your service worker.
Service workers: $service-worker is gone
If you built a PWA, this is your section. The odd $service-worker module is removed. You now import what you need from $app/env, $app/paths, and the new $app/manifest module, the same modules every other part of your app uses. You can also import self from $app/service-worker for accurate typings, provided your service worker's tsconfig extends $app/tsconfig/service-worker.
Related: SvelteKit now registers your service worker with type: 'module', since module service workers are widely supported. The RC post hints that caching-strategy helpers may arrive in a future release, which would make offline-friendly PWAs meaningfully less painful.
Environment variables become explicit and validated
SvelteKit 3 graduates the explicit environment variables feature out of the experimental flag. The model shifts from "scan and hope" to "declare and verify":
-
Declare the environment variables your app depends on in
src/env.ts. - Mark each one as publicly available or server-only, and as resolved at build time or at boot.
- Validate them with any Standard Schema library such as Zod or Valibot.
The payoff, per the RC announcement: type-safe, secure, validated environment variables that can be auto-imported where needed, with build-time resolution enabling dead code elimination. For anyone who has shipped a missing API key to production and found out at runtime, this is the quiet headline feature of the release.
Error handling is the upgrade most people will feel
This is where the Svelte 5 requirement pays off, and where I expect the most developer happiness per line changed:
-
Real error boundaries. SvelteKit 2 could only show your
+error.sveltecomponent for errors during load, because Svelte 4 had no error boundary concept. With Svelte 5's error boundaries, render errors are handled consistently too. -
handleError sees everything. Errors you deliberately created with
error(...)were previously ignored by yourhandleErrorhook on the assumption you had handled them. Now all errors flow through it, which means one place for logging and reporting. - Sourcemaps on stack traces. Production stack traces get properly sourcemapped. The RC post cautions it will take a while before every adapter displays them correctly, but the plumbing is in.
Shallow routing moves to goto
pushState and replaceState are deprecated in favor of goto with options. goto('/foo', { shallow: true }) updates the URL and page state without a real navigation, and persistState: true re-applies page.state after a reload. Shallow navigations now correctly trigger beforeNavigate and friends, and invalidateAll is deprecated in favor of refreshAll, which does not wipe your page state. Small API churn, but it touches modals, wizards, and filters in typical apps.
Vite 8 with Rolldown is mandatory
SvelteKit 2 supported Vite 8; SvelteKit 3 requires it. That means faster builds through Rolldown, the Rust-based bundler, and adoption of the Vite Environment API internally. One honest caveat from the Svelte team: they do not support FetchableDevEnvironment because it forces frameworks to absorb too much complexity, and they are exploring other ways to solve the Cloudflare Workers local-development problem. If your stack depends on that specific API, read the RC post before migrating.
Small traps worth knowing before they cost you an afternoon
From the migration guide, the changes that are easy to miss:
-
External redirects must be opted into.
redirect()to an external URL now requires{ external: true }or an allowlist of origins. javascript: URLs stay blocked. -
Links to the current page refresh. Clicking a link to where you already are triggers
refreshAll()instead of doing nothing. -
data-sveltekit attributes use false. The
'off'value is removed; writedata-sveltekit-preload-data="false". -
Version polling is on by default.
version.pollIntervalnow defaults to one hour, so apps will detect new deployments without you configuring anything. -
Enhanced forms navigate on cross-page actions. A form with
use:enhanceposting to an action on a different page now navigates there, matching native behavior.
The actual migration, condensed
The official command does the mechanical work and leaves a TODO list for the rest:
npx sv migrate sveltekit-3
And here is the full checklist in one place:
- Upgrade to the latest 2.x and fix every deprecation warning.
- Bump Node to 22.17+, TypeScript to 6+, Svelte to 5.56.4+, Vite to 8.0.12+, vite-plugin-svelte to 7+.
- Move
svelte.config.jscontents into thesveltekit()plugin options invite.config.ts; delete dead options likepreloadStrategy. - Add the
#libsubpath imports topackage.json; replace$liband add file extensions to#libimports. - Point
tsconfig.jsonat$app/tsconfigand set explicitinclude/exclude. - Port service workers off
$service-workeronto$app/env,$app/paths,$app/manifest. - Declare environment variables in
src/env.tsand wire up validation. - Replace
pushState/replaceStatewithgotoshallow options; swapinvalidateAllforrefreshAll. - Add
externaloptions to any external redirects; replace'off'withfalsein data-sveltekit attributes. - Decide deliberately whether hourly version polling suits your app.
One more expectation-setter: remote functions, the feature the Svelte team says makes "everything else look a bit clunky," are still behind an experimental flag in 3.0. Do not plan your architecture around them yet.
Is it worth it?
For most apps, yes, and the reason is not any single feature. It is that the migration is mostly mechanical, the tooling catches mistakes with diagnostics, and you land on Rolldown builds plus a genuinely better error-handling story. The changes also read like a team clearing technical debt on purpose: fewer special cases, more platform primitives, one config file instead of two. That is the kind of major version that ages well.
I write about the tools and frameworks I actually ship with, including Svelte, TypeScript, and backend engineering, every week. If that sounds useful, subscribing is free, and it is the one reliable way to see the next piece.
Have you run the SvelteKit 3 migration yet? I am especially curious whether the #lib extension requirement bit anyone with a large codebase. Tell me in the comments.
Top comments (0)