Skip to main content
ASoc
Comparison

Next.js vs SvelteKit: Where a Write Is Allowed to Live

Bundle size is the wrong axis for a meta-framework choice. The real divide is routing defaults and whether a mutation can live anywhere or only on the route that owns it.

The ASoc Team9 min read

Most "Next.js vs. SvelteKit" comparisons settle it on bundle size — we've already published that half, and it's the wrong axis for choosing a meta-framework anyway, because bundle weight is a component-library question, not a routing one. Naming the layers correctly matters in the neighbouring comparison too — "SolidJS vs. Next.js" is a UI library against a meta-framework, and the real pairing is SolidStart. The axis that actually differs here is where a mutation is allowed to live: a Next.js Server Action is a portable function callable from any Client Component, while a SvelteKit form action is pinned to the +page.server.ts that owns its route — a design difference this repo's own file layout makes concrete.

The decision in one table

Next.js App RouterSvelteKit
Route unitA folder under app/, page.tsx + optional layout.tsxA folder under src/routes/, +page.svelte + optional +layout.svelte
Static vs. dynamicPer-route, inferred from what the route reads (//ƒ in the build output)Per-route, declared via export const prerender
Data loadingasync Server Components fetch directly in the component tree+page.server.ts exports a load function, passed down as data
WritesServer Actions — plain async functions, "use server", callable from anywhereForm actions — exported from the route's own +page.server.ts, invoked via <form>
Where a mutation can liveAny module a Client Component importsOnly the +page.server.ts that owns the URL
DeploymentVercel-native; other hosts via adaptersAdapter-based for every target, including Vercel
Metadata routesFile convention (opengraph-image.tsx) with its own generateStaticParamsCustom endpoint (+server.ts) you wire yourself

Neither model is more "correct." They encode a different opinion about where request-shaped code is allowed to live, and that opinion shows up the moment you go looking for a specific piece of logic in a codebase you didn't write.

What "static by default" costs to get right in each

This storefront prerenders 111 product pages, 7 category hubs and 88 blog posts. A fresh production build's routing manifest — pulled while writing this post — reports 37 static routes and 3 dynamic route patterns, with the compiled rewrites object empty in all three phases:

"rewrites": { "beforeFiles": [], "afterFiles": [], "fallback": [] }

Getting there took four separate triggers we had to learn to spot, because Next.js gives you exactly one piece of feedback — a single character (, , or ƒ) next to each route in the build output — and no warning when a page quietly falls out of static rendering. Reading searchParams in a page, a per-response value from middleware, cookies() in a shared layout, or a metadata route missing its own generateStaticParams: all four are legal Next.js, all four are invisible until the build table, and all four are opt-outs from a static default rather than opt-ins to a dynamic one.

SvelteKit inverts which direction requires a declaration. A route is server-rendered per request unless you explicitly mark it:

// +page.ts
export const prerender = true;

There's no equivalent silent-opt-out risk, because there's nothing to accidentally trigger — reading a cookie or a query param in a load function doesn't change the route's prerender status behind your back the way it does in Next.js. The trade is the opposite failure mode: a page you meant to prerender stays server-rendered forever if nobody remembers to add the export, and nothing in the build fails loudly either way. Both frameworks put the risk in a place a build can't catch automatically; they just disagree about which default is the trap.

Where a write is allowed to live

This is the sharper divide, and it's a real architectural constraint rather than a taste difference. This codebase runs its writes through eight files in src/lib/actions/account.ts, auth.ts, checkout.ts, contact.ts, entitlementsView.ts, newsletter.ts, redemption.ts, refund.ts — each exporting one or more "use server" functions. The full case for that split against the two Route Handlers this app also carries is its own post; the relevant fact here is that any of those eight files can be imported into any Client Component, anywhere in the tree, because a Server Action is just a function reference the bundler swaps for a network call.

SvelteKit's form actions don't have that freedom. They're exported from the actions object inside the +page.server.ts that owns the route the <form> is rendered on:

// src/routes/checkout/+page.server.ts
export const actions = {
  default: async ({ request }) => {
    const data = await request.formData();
    // ...
  },
};

Put that export in +layout.server.ts expecting it to apply to every child route, and SvelteKit silently ignores it — the action has to live on the specific +page.server.ts whose URL the form submits to. That's the same "request-shaped code is invisible until you check" failure mode Next.js has with a dynamic-scope layout, just moved to a different file convention: Next.js will happily let you put a Server Action anywhere and it works from everywhere, which is flexible right up until ownership has to come from the verified session rather than a parameter — a Server Action is a public POST endpoint no matter which file it's defined in, so the flexibility that makes it easy to call from anywhere is also what makes "which action is this, and who's allowed to call it" a discipline you have to hold yourself, rather than one the file layout enforces for you the way SvelteKit's route-owned actions do.

The part neither framework's docs will tell you

Reading both docs side by side, the thing that doesn't show up in either is what happens when the pattern is misused rather than followed. Next.js's flexibility means a Server Action imported into an unrelated component compiles and runs fine — there's no build-time signal that it's now reachable from a page that never intended to expose it. SvelteKit's route-ownership means a misplaced action in a layout fails silently at runtime with no error, just a form submit that does nothing. Neither is a lint rule today; both are the kind of thing worth a comment at the call site, the same way this repo's own dashboard tab handler leaves a comment explaining a non-obvious focus-management rule rather than trusting the next reader to rediscover it.

Where SvelteKit's model is the better answer

  • A form-heavy app where every mutation maps cleanly to one route. SvelteKit's ownership model is free discipline in exactly that shape — there's no way to accidentally wire a checkout action to render from the wrong page.
  • A team that wants the compiler to do more of the reactivity work. Already covered in full: Svelte's compiled output has no virtual-DOM runtime to ship, which matters most for pages that are mostly interactive rather than mostly static.
  • Smaller, more opinionated deployment targets. SvelteKit's adapter model is uniform across hosts; Next.js's feature surface (Server Actions, metadata routes, ISR) is deepest on Vercel specifically.

None of those describe a 111-product SSG catalog whose writes are eight well-scoped commerce actions behind Supabase auth — which is why this comparison is a routing-and-mutation-model fit question, not a verdict on either framework.

Mistakes and how they show up

MistakeSymptomFix
Assuming a Next.js page stays static by default foreverƒ appears in the build table with no error, no warningDiff the route legend on every build; know the four common dynamic triggers
Putting a SvelteKit form action in +layout.server.tsThe form submits and nothing happens — no errorActions live only on the +page.server.ts that owns the URL
Treating a Server Action's portability as freeIt's callable from anywhere, including places that shouldn't own it, with no build-time signalRe-verify the session inside every action; never trust the caller
Comparing the two frameworks on bundle size aloneMisses the routing and mutation-ownership differences that matter more for a form-heavy or content-heavy appWeigh where request-shaped code is allowed to live, not just what ships to the client
Assuming SvelteKit's per-route load function behaves like a Next.js Server Component fetchDifferent caching and revalidation model underneath a similar-looking APIRead each framework's actual data-loading lifecycle before porting a pattern

Frequently asked questions

Is this the same comparison as "React vs. Svelte"? No — that post is about bundle weight and how Server Components change what fraction of a page ships client JavaScript. This one is meta-framework: routing conventions, static/dynamic defaults, and where a write is allowed to live. They're deliberately different axes on the same underlying frameworks.

Which framework makes it harder to accidentally ship a security bug? Neither does it for you automatically. Next.js's Server Action portability means you must re-check the session inside every action regardless of where it's called from; SvelteKit's route-owned actions narrow where a mutation can be triggered from but still require the same session check inside the action itself. Route ownership is not authorization.

Does SvelteKit have an equivalent to Next.js's static/dynamic route table? Not the same single-character build output. prerender is a per-route export you set explicitly, so the practical difference is that Next.js infers dynamic behavior implicitly (and surprises you) while SvelteKit requires you to declare static behavior explicitly (and silently under-prerenders if you forget).

Would this comparison change if the app were more interactive? The routing half wouldn't — the file-convention and mutation-ownership differences hold regardless of how interactive the app is. The bundle-size half would, in Svelte's favor, which is exactly what the React vs. Svelte post already covers for that case.

Templates in this post

ASoc Mind is an AI marketing-solutions site — image-AI services, projects, team and achievements — the kind of mostly-static, form-light build where Next.js's static-by-default routing costs nothing to get right. ASoc Momentum is an AI-consulting agency site with services, case studies and a contact funnel — one real write path, the shape the Server Actions comparison is about. ASoc Neuron is a neural-networks AI-platform site with six capabilities and 3-tier pricing, entirely server-rendered content with no mutation surface at all.

Browse the full sets: Next.js landing page templates, Tailwind landing page templates. For the bundle-size half of this comparison, React vs. Svelte; for how this codebase decides what stays static, static rendering in the App Router. For the framework neither side of this comparison covers, Angular vs. Svelte puts the same reactivity question to Angular's DI-and-signals model.

Keep reading

Comparison8 min read

Next.js vs. Vue: The Reactivity Primitive You Don't Need

Vue's Composition API needs ref/computed for every stateful value, server or client. This storefront's 13 dependencies carry zero state library — here's why.

Read more
Comparison7 min read

One-Time Payment vs Subscription: What the Webhook Handler Has to Know

Our entitlement engine is 75 lines with two status states; our LemonSqueezy webhook recognizes two event names. That's not a shortcut — it's the real cost gap between the two pricing models.

Read more
Comparison8 min read

Playwright vs Vitest: Why This Repo Runs Both for Unrelated Jobs

Vitest is a committed dependency gating 305 tests on every push. Playwright is deliberately not a dependency at all — installed ad hoc for a script that runs a few times a year.

Read more