Next.js vs. Nuxt: 24 of 92 Components Opt Into the Client
Not React versus Vue — the real split is where each framework lets you draw the server/client boundary. Next.js draws it through the import graph; Nuxt's default is universal.
Next.js and Nuxt are the same kind of tool — a file-routed, server-rendering framework wrapped around a view library — so the choice is not React versus Vue. It is where each one lets you draw the server/client boundary. Next.js draws it through the module graph with "use client". Nuxt draws it per component file, because its default is universal.
That difference is measurable. In this storefront, 24 of 92 component files carry "use client"; the other 68 ship no client JavaScript for their own logic at all. A Nuxt port of the same app would not have 68 components in that state, because a Vue component in a universal Nuxt app runs on the server and in the browser by default.
The decision in one table
| Next.js App Router | Nuxt | |
|---|---|---|
| Default execution | Server-only. A component is a Server Component until something opts out | Universal. A component renders on the server, then hydrates and runs again on the client |
| How you opt out | "use client" at the top of a module — a boundary in the import graph | A file suffix (.client.vue / .server.vue) or a <ClientOnly> wrapper — a boundary around one component |
| What the boundary covers | Everything imported below it becomes client code, transitively | Only the file or subtree you marked |
| Reading data | await a function inside an async component; nothing is serialized for the client | A composable (useAsyncData / useFetch) whose result is serialized into the page payload for hydration |
| Server-only routes | Route Handlers under app/api/ | Nitro handlers under server/api/ |
| Request-time hook | src/proxy.ts (Next 16's rename of middleware) | Nitro server middleware under server/middleware/ |
| Deployment | Adapter-shaped; the host is expected to understand the build output | Nitro presets target many hosts from one build |
| Routing convention | Folders plus reserved filenames (page, layout, route, error…) | Files under pages/, plus auto-imported components and composables |
Both are good frameworks and the table has no loser in it. The rows that matter are the first three, and they compound.
The boundary is an import graph, not a file
This is the part people port incorrectly. In Next.js, "use client" does not mark a component as client-side. It marks the point where the module graph crosses into the bundle. Everything that module imports — and everything those modules import — is client code, whether or not it says so.
That is why the boundary in this codebase sits so low. RelatedProducts is the rail of six sibling products that closes every product page, and it is a plain Server Component:
// src/components/organisms/RelatedProducts.tsx
import RelatedTemplateCard from "@/components/molecules/RelatedTemplateCard";
import { catalog, FRAMEWORK_LABELS, type TemplateProduct } from "@/data/catalog";
/** How many siblings each product links to. */
const RAIL_SIZE = 6;
It imports the whole 111-product catalog and renders six cards from it. None of that array reaches the browser. The component runs once at build time, emits HTML, and the catalog module never enters a bundle — so the cost of importing a large data file into a Server Component is zero bytes on the client.
It renders RelatedTemplateCard, deliberately not the interactive TemplateCard. That is the module-graph rule doing its work: TemplateCard pulls in a preview modal, a wishlist toggle and an ownership lookup, so using it six times per page would drag all three into the client bundle for what is only a navigation rail. In Nuxt the equivalent decision exists but is scoped differently — you would reach for a <ClientOnly> wrapper around the interactive parts, or split the card into two components, because marking a parent does not transitively pull its children into a bundle the way a Next.js client boundary does.
Neither model is obviously better. Next's is stricter and catches the transitive case for you; Nuxt's is more local and easier to reason about one file at a time.
What the default costs, counted
A fresh production build of this site:
$ node -e "const m=require('./.next/prerender-manifest.json');
console.log(Object.keys(m.routes).length, 'prerendered routes');"
444 prerendered routes
Against those 444 prerendered routes sit exactly 8 dynamic route patterns — /api/download, /api/webhooks/lemonsqueezy, /auth/callback, /dashboard, /dashboard/settings, /login, /reset-password and /signup. That is the entire surface that has to run per request: the account and commerce paths, and nothing else.
The component census behind those routes:
| Measure | Count |
|---|---|
Component files in src/components | 92 |
…carrying "use client" | 24 |
Files importing server-only | 33 |
Files with "use server" | 19 |
| Runtime dependencies | 13 |
page.tsx files producing 444 URLs | 27 |
Nuxt can reach a comparable static count — Nitro's prerenderer is good, and a mostly-content site is exactly its strength. The difference is not whether the HTML can be generated. It is that 68 of these component files have no client-side existence to reason about, so questions like "does this run before hydration?" and "is this value serialized into the payload?" simply do not arise for them. In a universal Nuxt app those questions apply to every component that has not been explicitly narrowed.
Data fetching: a function call versus a payload
In Next.js, a Server Component reads data by calling a function. There is no fetch layer, no cache key, and nothing crosses the wire:
export default async function Page({ params }) {
const { slug } = await params;
const product = getProduct(slug); // a plain lookup in an in-memory array
return <TemplateDetail product={product} />;
}
Nuxt's equivalent is a composable — useAsyncData or useFetch — which runs on the server during SSR and then serializes its result into the page payload so the client can hydrate without refetching. That serialization is the tradeoff. It is what makes a Nuxt page resumable on the client, and it is also a real byte cost that scales with the size of the data you fetched.
This site has already measured the same cost on the React side: the home page's prerendered HTML is 436 KB, and 52.1% of it is the serialized render rather than the page. Both frameworks pay a hydration-payload tax. The difference is that in Next.js you can drive it to zero for a given subtree by keeping it server-only, whereas in Nuxt the universal default means opting out is the deliberate act.
The request-time hook, and where Nuxt is genuinely nicer
This codebase has one file that runs on every request. In Next 16 it is called a proxy:
// src/proxy.ts — 42 lines
export async function proxy(request: NextRequest) {
let response = NextResponse.next({ request });
// ...createServerClient wiring...
await supabase.auth.getClaims();
return response;
}
export const config = {
matcher: [
"/((?!_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt|.*\\.(?:svg|png|jpg|jpeg|webp|gif|ico|webmanifest|html)$).*)",
],
};
That matcher regex is the tell. It is a hand-maintained exclusion list, and it exists because the hook is defined by path pattern rather than by the routes it belongs to. Nuxt's server/middleware/ has the same job and the same shape of problem, but Nitro's routing gives you defineEventHandler with route rules declared alongside the rest of the server config, which many people find easier to keep correct than a negative-lookahead regex.
Nitro is the honest advantage in Nuxt's column more broadly. One build, many deployment presets, with the server layer as a first-class piece of the framework rather than something the host is expected to interpret. If you are deploying somewhere unusual, that is a real reason to pick Nuxt and it has nothing to do with Vue.
Where each one is the better answer
Pick Nuxt if your team writes Vue, you want auto-imports and a single opinionated way to do things, or your deployment target is outside the well-trodden Next.js hosting path and Nitro has a preset for it.
Pick Next.js if the app is mostly content that should be static and you want the framework to prove which parts are — the 444/8 split above is a build-time fact, not a configuration promise — or if you want the server/client boundary enforced transitively rather than per file.
Neither choice is a lever on the thing most people expect. Both prerender well, both do SSR, both have file-based routing, and both will produce a fast site. This site's whole stylesheet is 76,858 bytes raw and 13,858 gzipped across all 444 routes; that number comes from Tailwind, not from the meta-framework, and it would be identical under Nuxt.
Mistakes and how they show up
| Mistake | What happens | The fix |
|---|---|---|
Porting a Nuxt component tree by adding "use client" at the top of a page | The entire page subtree becomes client code, including every data import below it | Push the boundary down to the leaf that actually needs interactivity |
Expecting <ClientOnly> to have a "use client" equivalent scope | <ClientOnly> wraps one subtree; "use client" is transitive through imports | Treat them as different mechanisms, not translations of each other |
Reaching for useAsyncData's shape in a Server Component | You add a fetch and a cache key where a plain function call would do | If it runs on the server, just call the function |
| Assuming a large data import costs client bytes | It does not, in a Server Component — catalog never enters a bundle | Check the bundle, not the import statement |
| Comparing the two on runtime bundle size | Measures the view libraries, not the frameworks, and ignores components that ship nothing | Count what reaches the client per route |
Frequently asked questions
Is Next.js vs. Nuxt the same question as React vs. Vue? No, and mixing them is why most comparisons go wrong. React versus Vue is about how you author a component and its state — that comparison is Next.js vs. Vue, which turns on the reactivity primitive. Next.js versus Nuxt is about routing, rendering defaults, data fetching and deployment, which is what this post covers.
Does Nuxt have React Server Components? Not in the same sense. Nuxt server components render once on the server with no client-side interactivity, which makes them closer to a static include than to a component you can selectively hydrate. React's split lets one page tree mix both, decided per component — the mechanism behind the 24-of-92 number above.
Can Nuxt produce a fully static site like these 444 routes? Yes. Nitro prerendering handles this well and a content-heavy site is a good fit for it. The difference is not the output but the default: here, static is what a route is unless it touches something request-scoped, and the 8 dynamic routes are the ones that do.
Which has better TypeScript support?
Both are strong. Nuxt generates types for routes, composables and auto-imports; Next.js types flow through the same .tsx file the component is written in. This has not been a deciding factor for a few versions now.
Templates where this ships
ASoc Uptime is a web-hosting template built on the static-by-default model this post describes. ASoc Vault is a fintech SaaS landing page with the same boundary discipline, and ASoc Vox is an AI voiceover landing page on the identical foundation — all three prerender in full, with client JavaScript added only where a section genuinely needs it.
Browse the full sets: Next.js landing page templates, Tailwind landing page templates. For the rendering triggers behind the 444-route count, read static rendering in the App Router; for the hydration payload both frameworks pay, read what actually reaches a crawler.
