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.

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

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
Comparison9 min read

Qwik vs. Next.js: Resumability vs. Not Shipping the Component

Qwik ships near-zero JS through resumability; this site's RSC split already does that for 9 of 10 home-page sections. Where the real cost still lands: one accordion.

Read more