Skip to main content
ASoc
Comparison

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.

The ASoc Team8 min read

Next.js and Vue answer a different question first. Next.js asks "does this component need to be interactive at all" and defaults to no — a Server Component ships zero client JavaScript unless it opts in. Vue asks "how do I make this value reactive" for every stateful piece, client or server, because reactivity is Vue's whole model. On this storefront that difference shows up as a number: 13 runtime dependencies, and not one of them is a state-management or data-fetching library.

The decision in one table

Next.js App RouterVue 3
Default rendering modeStatic, per route, inferred from what the route touchesClient-reactive by default; SSR/SSG needs Nuxt
Stateful valueuseState, scoped to one Client Componentref()/reactive(), explicit .value access, works the same on server or client
Derived valueA plain expression, recomputed on rendercomputed(), a distinct primitive with its own caching semantics
Component format.tsx — JSX compiled by the same pipeline as everything else.vue Single-File Component — template, script and style in one file, its own compiler step
Reaching the server for dataasync Server Component, no client roundtrip, no storeComposable + fetch/useFetch (Nuxt) or a manual store (Pinia)
Global client stateNot needed anywhere in this codebase — zero Redux/Zustand/Pinia-equivalent dependencyCommonly Pinia once state crosses more than one component tree

Vue's reactivity system is genuinely good at what it does — ref() and computed() give you fine-grained, automatic dependency tracking with none of React's manual-memoization tax. That's a real advantage for a component that's reactive by nature: a live filter count, a form that revalidates on every keystroke. The tradeoff is that the primitive is required everywhere state exists, including inside a .vue file that never leaves the server, because Vue doesn't have a component type that skips reactivity the way a Server Component skips the client runtime entirely. Which primitive that should be is its own decision — ref vs. reactive, settled here by counting all 33 of this codebase's state values and finding 30 of them primitives that reactive() cannot hold at all.

What that looks like in a real file

This storefront's /templates filter grid is the one place in the codebase that holds meaningfully interactive client state — a set of chip filters over 111 products — and it's a single useState call, no companion primitive:

// src/components/organisms/TemplatesExplorer.tsx
"use client";
export default function TemplatesExplorer() {
  const [filters, setFilters] = useState<TemplateFilters>({});
  const results = filterTemplates(catalog, filters);
  // ...
}

results is not memoized, not wrapped in anything, not declared with a second primitive for "this depends on filters" — it's a plain function call that re-runs on every render, because filterTemplates over 111 in-memory objects costs nothing worth caching. The Composition API equivalent needs two primitives working together where this needs one:

// The Vue 3 Composition API shape of the same component
const filters = ref<TemplateFilters>({});
const results = computed(() => filterTemplates(catalog, filters.value));

That's not a knock on Vue's code — it's four lines either way, and computed()'s caching is arguably the more correct default for anything that isn't as cheap as filtering an array in memory. The actual gap is upstream of this component: every other section of /templates — the header, the category copy, the whole page shell — is a Server Component that ships no JavaScript and needs neither useState nor ref(). This codebase's Client Component census, measured for the Svelte comparison, puts 28 of 33 organisms at zero client JS. A .vue file has no equivalent tier to render into without stepping outside Vue itself into Nuxt's server components, which are a different, newer, more restricted primitive (no client interactivity at all, not "interactivity you opted out of").

The Single-File Component tradeoff

Vue's .vue file bundles <template>, <script setup> and <style scoped> in one file, compiled by its own toolchain (@vitejs/plugin-vue or the Nuxt equivalent) before anything reaches the browser. That's a genuine authoring convenience — a component's markup, logic and scoped styles are three sections of one file instead of a .tsx file plus whatever styling mechanism the project has chosen. This codebase's .tsx files carry no separate style block at all; Tailwind's utility classes live inline in the JSX, so there's no third section to colocate and no separate compiler stage between the file you write and the JSX the framework consumes — one less toolchain step, at the cost of losing the template/logic/style separation .vue files offer for a team that prefers it explicit.

Where Vue is straightforwardly the better answer

To be direct about the side of this that doesn't favor Next.js: Vue's learning curve is real and shorter. <script setup> reads close to plain JavaScript, template syntax is closer to HTML than JSX is, and a team that doesn't already know React pays no tax learning React's rendering model, hook rules, or the Server/Client Component split this whole post is about. Vue's official ecosystem is also more centralized — routing (vue-router), state (pinia) and meta-framework (nuxt) are all maintained by the same core team with matched release cadences, where the React ecosystem's equivalent pieces are chosen independently. If the project is small, the team is Vue-native, and nothing about the routing needs Next's per-route static/dynamic inference, Vue with Nuxt is not a worse choice — it's a different one.

The build this comparison is measured against

A fresh production build of this site prerenders 352 routes with 8 dynamic route patterns (/api/download, /api/webhooks/lemonsqueezy, /auth/callback, /dashboard, /dashboard/settings, /login, /reset-password, /signup — the account and commerce surface, the only part of the app that has to run per request):

$ node -e "const m=require('./.next/prerender-manifest.json');
  console.log(Object.keys(m.routes).length, 'prerendered routes');"
352 prerendered routes

None of that static output required a reactivity primitive to produce — generateStaticParams runs once at build time over the catalog array, and the resulting pages carry no runtime that needs to track a dependency graph at all. Vue's own SSG story (via Nuxt's nitro prerender) can reach a similar static count, but every prerendered .vue page is still built from components written in the reactive primitive, because that's the only component type Vue has.

Common mistakes

MistakeSymptomFix
Assuming "Vue is a frontend framework, Next.js is a meta-framework" is the whole comparisonMisses that Nuxt is Vue's meta-framework, and the fair comparison is Next.js vs. Nuxt for routing/SSRCompare Next.js to Nuxt when routing is the question, Next.js to Vue-the-library when component authoring is
Reaching for ref()/computed() inside a component that never needs to re-renderReactivity overhead paid for content that's static after mountIf nothing changes after first render, it doesn't need a reactive primitive — server-render it
Treating bundle-size comparisons as settled by "Vue is smaller"Ignores that a Server Component ships zero bytes for its own logic, smaller than any client-rendered Vue componentCount what actually reaches the client, not the runtime's base size
Porting a Pinia store 1:1 into a Next.js appRecreates global client state this codebase has never neededCheck whether a Server Component can just read the data directly first

Frequently asked questions

Is this really "Next.js vs. Vue" or "Next.js vs. Nuxt"? Both, depending on what you're deciding. For component authoring — how you write a piece of UI and its state — it's Next.js's React (with Server Components) vs. Vue's Composition API, which is the comparison this post makes. For routing, SSR and static generation, the fair opponent to Next.js is Nuxt, since plain Vue has no built-in server-rendering story.

Does Vue have anything like a Server Component? Nuxt server components exist, but they render once on the server with no client-side interactivity at all — closer to a static include than a component you can later hydrate selectively. React's Server/Client Component split lets a single page tree mix both, decided per component, which is the mechanism behind this codebase's 352 static routes each still carrying interactive pieces where they're needed.

Which one has better TypeScript support? Both are strong today. Vue's <script setup lang="ts"> template type-checking has closed most of the gap that used to exist against JSX, where types flow through the same .tsx file the component is written in. Neither is a deciding factor anymore the way it was a few versions ago.

Do I need Pinia if I pick Vue? Only once state needs to cross component trees that don't share a parent — the same threshold that would push a React app toward Context or a state library. This codebase has never crossed it: zero state-management dependency across 13 runtime packages, because the pieces that need to talk to each other are colocated in one Server Component tree that reads the catalog directly.

Templates where this ships

ASoc Fade is a barbershop booking site built on exactly the static-by-default model this post describes. ASoc Fiscal is a financial-platform landing page with the same zero-client-state architecture. ASoc Flow automates workflow marketing copy on the identical foundation — all three prerender in full, with interactivity added only where a section actually needs it.

Browse the full sets: Next.js landing page templates, Tailwind landing page templates. For the Server/Client Component split this post leans on, read the React vs. Svelte census; for the static-rendering triggers behind the 352-route count, read static rendering in the App Router.

Keep reading

Comparison9 min read

Next.js vs Wix: The Line Is What You Can Export

Both can be fast and both can look good. The decision is whether your pages are derived from your own data — and whether the source code leaves with you.

Read more
Comparison9 min read

Nuxt vs Bubble: Which Half of the Work Do You Want to Own?

Bubble includes the expensive half: auth, data model, workflows. Priced in this repo's own files — 8 migrations, 6 RLS policies, 31 test files — and what code buys back.

Read more