You've read ten "Next.js vs Nuxt" articles that all say "it depends." Frustrating, right?
Here's the thing: they're correct—but they never tell you what it depends on. This guide does. We'll compare real benchmark data, show you exactly where each framework wins, and give you a clear decision framework for your next project.
The short answer (with numbers)
Neither framework is universally faster. The benchmarks tell a more interesting story:
| Workload | Winner | Margin |
|---|---|---|
| Simple SSR pages | Next.js | +87% faster |
| Pages with API calls | Nuxt | +144% faster |
| Edge cold starts | Nuxt | ~2ms vs ~30ms |
| Bundle size (baseline) | Nuxt | ~33% smaller |
The framework you choose matters less than how you use it. But if you know your workload, you can pick the one optimized for it.
The benchmark data (real numbers)
Enough hand-waving. Here are actual requests-per-second measurements for server-side rendering:
| Scenario | Nuxt 3 | Next.js | Difference |
|---|---|---|---|
| Simple "Hello World" | 1,376 req/s | 2,570 req/s | Next.js +87% |
| Rendering a component | 1,447 req/s | 2,794 req/s | Next.js +93% |
| Page with API fetch | 947 req/s | 388 req/s | Nuxt +144% |
What this means:
Next.js's raw rendering speed is faster for simple pages. But most real apps fetch data, and Nuxt handles that workload significantly better.
Why the difference? Nuxt's Nitro server is optimized for data fetching patterns. Next.js's strength is in raw React rendering speed, but that advantage disappears when I/O is involved.
Build and dev server speed
Development experience matters. Slow builds kill productivity and make developers avoid running the dev server.
Nuxt: Vite-powered
Nuxt 3 uses Vite, which leverages native ES modules for near-instant hot module replacement. Unlike traditional bundlers, Vite doesn't bundle your code during development - it serves ES modules directly to the browser.
// nuxt.config.ts
export default defineNuxtConfig({
// Vite is the default - no config needed
// HMR is nearly instant out of the box
vite: {
// Optional: customize Vite config
optimizeDeps: {
include: ['some-cjs-dependency'],
},
},
})
Nuxt/Vite development characteristics:
- Cold start: 300-800ms for typical apps
- HMR: Usually under 100ms
- Memory usage: Lower than Webpack-based setups
- Nuxt claims 80% faster HMR than Webpack-based Next.js
The tradeoff: Vite's unbundled approach means your browser makes hundreds of HTTP requests during development. This is fine locally but can feel slow over poor network connections (like remote development servers).
Next.js: Turbopack
Next.js is transitioning from Webpack to Turbopack, a Rust-based bundler written by the same team that created Webpack:
// next.config.js
module.exports = {
// Turbopack is now stable for dev
// Enable with: next dev --turbo
}
Turbopack characteristics:
- Cold start: 200-500ms (faster than Vite for large apps)
- HMR: Sub-100ms with incremental compilation
- Memory usage: Efficient due to Rust's memory management
- Particularly fast for large monorepos (1000+ modules)
Turbopack's incremental computation model means it only recomputes what changed. For large applications with complex dependency graphs, this can be dramatically faster than both Vite and Webpack.
# Enable Turbopack
next dev --turbo
# You'll see in the console:
# ▲ Next.js 15.x (turbopack)
# - Local: http://localhost:3000
Production build comparison
| Metric | Nuxt (Vite/Rollup) | Next.js (Turbopack/Webpack) |
|---|---|---|
| Small app build | 5-15s | 10-30s |
| Large app build | 30-60s | 45-90s |
| Incremental rebuild | Fast | Fast with Turbopack |
| Tree-shaking | Excellent (Rollup) | Good (improving) |
The verdict
For most projects, both are fast enough that you won't notice a difference. For enterprise-scale monorepos with thousands of modules, Turbopack has demonstrated faster cold starts and HMR. For typical projects, Vite's mature ecosystem, plugin library, and stability make it a safe choice.
If you're starting fresh and prioritize build speed, both frameworks are competitive. Choose based on React vs Vue preference, not build tools.
Bundle size comparison
Bundle size directly affects LCP - smaller bundles mean faster page loads.
Framework runtime size
Vue's runtime is roughly 33% smaller than React's:
| Framework | Minified + Gzipped |
|---|---|
| Vue 3 | ~33 KB |
| React 18 + ReactDOM | ~44 KB |
This baseline difference means Nuxt apps start with a smaller JavaScript footprint.
But Server Components change the equation
Next.js's React Server Components can dramatically reduce client-side JavaScript by keeping components on the server:
// Next.js - This component ships ZERO JS to the client
async function ProductList() {
const products = await db.products.findMany();
return (
<ul>
{products.map(p => <li key={p.id}>{p.name}</li>)}
</ul>
);
}
When used effectively, RSC can make Next.js bundles smaller than equivalent Nuxt apps despite React's larger runtime.
Nuxt's auto-imports
Nuxt's auto-import system uses build-time analysis to tree-shake aggressively:
<script setup>
// No imports needed - Nuxt handles it at build time
// Only what you use gets bundled
const { data } = await useFetch('/api/products')
</script>
This "magic" is analyzed statically, so it doesn't hurt bundle size.
The verdict
Out of the box, Nuxt produces smaller bundles. With disciplined use of Server Components, Next.js can match or beat Nuxt. Both require attention to achieve optimal bundle sizes.
Image optimization
Images are usually the biggest LCP factor. How each framework handles them matters.
Next.js: Built-in
Next.js has native image optimization:
import Image from 'next/image';
function Hero() {
return (
<Image
src="/hero.jpg"
alt="Hero"
width={1200}
height={600}
priority // Marks as LCP image
/>
);
}
Features included out of the box:
- Automatic WebP/AVIF conversion
- Responsive srcset generation
- Lazy loading by default
- Blur placeholder support
- Vercel's edge network optimization
Nuxt: Module required
Nuxt needs the @nuxt/image module:
npm install @nuxt/image
<template>
<NuxtImg
src="/hero.jpg"
alt="Hero"
width="1200"
height="600"
preload
/>
</template>
Once installed, it's equally capable - modern formats, responsive images, lazy loading. But it's an extra setup step.
The verdict
Next.js wins on convenience. Image optimization just works. Nuxt can match it, but you need to install and configure the module.
Server Components
Server Components are the biggest architectural difference between the frameworks.
Next.js: React Server Components (Mature)
RSC is the default in Next.js App Router. Components run on the server unless you opt into client-side:
// Server Component (default) - no JS sent to client
async function Dashboard() {
const data = await fetchDashboardData();
return <DashboardView data={data} />;
}
// Client Component - opt in with directive
'use client';
function InteractiveChart({ data }) {
const [zoom, setZoom] = useState(1);
return <Chart data={data} zoom={zoom} onZoom={setZoom} />;
}
RSC enables:
- Direct database access in components
- Streaming and Suspense
- Granular client/server boundaries
- Significant bundle size reduction
Nuxt: Server Components (Experimental)
Nuxt has server components, but they work differently:
<!-- components/ServerOnly.server.vue -->
<script setup>
// This component renders on server only
const data = await $fetch('/api/data')
</script>
<template>
<div>{{ data }}</div>
</template>
Nuxt's approach is simpler but less flexible. For most Vue patterns, you'll use useFetch and useAsyncData instead:
<script setup>
// Runs on server during SSR, cached for client
const { data } = await useFetch('/api/products')
</script>
The verdict
Next.js has more mature and flexible server component patterns. Nuxt's data fetching primitives (useFetch, useAsyncData) handle most use cases elegantly without needing explicit server components.
Edge performance
Edge computing serves pages from servers closest to users. Instead of one central server in Virginia, your code runs in 200+ locations worldwide. This reduces latency from 100-200ms to 10-30ms for most users.
Nuxt: Nitro's edge advantage
Nitro, Nuxt's server engine, was designed from the ground up for edge deployment. It compiles your entire Nuxt application into a minimal, self-contained bundle that runs anywhere.
// nuxt.config.ts
export default defineNuxtConfig({
nitro: {
preset: 'cloudflare-pages' // or 'vercel-edge', 'netlify-edge', 'deno-deploy'
}
})
Nitro's edge characteristics:
| Metric | Nitro on Cloudflare Workers |
|---|---|
| Cold start | ~2ms |
| Bundle size | ~700 KB for full app |
| Memory limit | Works within 128MB |
| Supported platforms | 15+ (Cloudflare, Vercel, Netlify, Deno, AWS Lambda, etc.) |
The ~2ms cold start is remarkable. Traditional serverless functions take 100-500ms to cold start. Nitro achieves this by:
- Compiling to optimized JavaScript (no Node.js runtime needed)
- Bundling dependencies inline (no npm install at runtime)
- Using the Web API standard (works natively on edge runtimes)
// Nitro compiles this to a single, optimized bundle
export default defineEventHandler(async (event) => {
const data = await $fetch('/api/products');
return { products: data };
});
Multi-platform flexibility:
Nitro's biggest advantage is deployment flexibility. The same Nuxt app can deploy to any edge platform without code changes:
# Deploy to Cloudflare
NITRO_PRESET=cloudflare-pages npm run build
# Deploy to Vercel Edge
NITRO_PRESET=vercel-edge npm run build
# Deploy to Netlify Edge
NITRO_PRESET=netlify-edge npm run build
Next.js: Vercel Edge
Next.js edge functions are optimized for Vercel's infrastructure:
// app/api/hello/route.js
export const runtime = 'edge';
export async function GET() {
return Response.json({ hello: 'world' });
}
// For pages
// app/page.js
export const runtime = 'edge';
export default function Page() {
return <div>Edge-rendered page</div>;
}
Next.js Edge characteristics:
| Metric | Next.js on Vercel Edge |
|---|---|
| Cold start | ~10-30ms |
| Bundle size | Varies by page |
| Memory limit | 128MB on Vercel |
| Supported platforms | Best on Vercel, limited elsewhere |
Next.js edge is fast but not quite Nitro-level for cold starts. However, Vercel's infrastructure compensates with:
- Aggressive function warming (keeping instances hot)
- Geographic routing optimization
- Integrated caching with the Next.js data cache
The platform lock-in consideration:
Next.js edge functions use some Vercel-specific APIs. Deploying to other platforms (Cloudflare, Netlify) requires adapters and may not support all features. This isn't a problem if you're on Vercel, but matters for multi-cloud strategies.
When edge matters
Edge deployment significantly improves performance for:
- Global audiences: Users in Asia see 100ms+ latency improvements when served from Singapore vs Virginia.
- Personalized content: Server-render personalized pages at the edge without client-side JavaScript.
- API routes: Database queries from edge locations closest to your database regions.
- A/B testing: Run experiments at the edge without client-side flicker.
When edge doesn't matter:
- Static sites (use a CDN instead)
- Apps with a single geographic audience
- Heavy computation (edge has CPU limits)
- Large dependencies (edge bundles have size limits)
The verdict
Nuxt/Nitro has faster edge cold starts (~2ms vs ~10-30ms) and better multi-platform support. For latency-critical applications deployed outside Vercel, Nuxt has a clear advantage.
On Vercel, Next.js's deeper integration (caching, analytics, function warming) may offset the cold start difference. If you're already committed to Vercel, Next.js edge works excellently.
For multi-cloud or vendor-neutral deployments, Nitro's universal preset system makes Nuxt the safer choice.
Core Web Vitals optimization
Here's how to optimize each framework for LCP, CLS, and INP. The techniques are framework-specific, but the principles are the same.
LCP optimization
LCP measures when your largest content element becomes visible. For both frameworks, the main factors are: image optimization, server rendering, and JavaScript bundle size.
Next.js approach:
Next.js has built-in image optimization that handles most LCP issues automatically:
import Image from 'next/image';
function HeroSection() {
return (
<section>
{/* priority tells Next.js this is the LCP image */}
{/* It disables lazy loading and adds preload hints */}
<Image
src="/hero.jpg"
alt="Hero"
width={1200}
height={600}
priority
/>
<h1>Welcome to our site</h1>
</section>
);
}
Additional Next.js LCP strategies:
- Use Server Components (default in App Router) to reduce client JS
- Enable streaming with
loading.jsfor faster TTFB - Use
next/fontto eliminate font-loading delays
// Streaming shows content progressively
// app/page.js
import { Suspense } from 'react';
export default function Page() {
return (
<main>
<HeroSection /> {/* Shows immediately */}
<Suspense fallback={<ProductsSkeleton />}>
<ProductList /> {/* Streams in when ready */}
</Suspense>
</main>
);
}
Nuxt approach:
Nuxt requires the @nuxt/image module for equivalent optimization:
<template>
<section>
<!-- preload tells Nuxt this is the LCP image -->
<NuxtImg
src="/hero.jpg"
alt="Hero"
width="1200"
height="600"
preload
/>
<h1>Welcome to our site</h1>
</section>
</template>
<script setup>
// Data fetched server-side, HTML sent immediately
const { data: hero } = await useFetch('/api/hero', {
server: true, // Only runs on server
lazy: false, // Blocks rendering until complete
})
</script>
Additional Nuxt LCP strategies:
- Use
useFetchwithserver: truefor SSR data - Lazy load below-fold components with
Lazyprefix - Use
@nuxtjs/fontainefor font fallback matching
CLS optimization
CLS measures visual stability. Both frameworks handle this similarly - the key is always specifying dimensions for media elements.
The universal rule: Every image, video, iframe, and dynamic container needs explicit dimensions.
// Next.js - Image component requires dimensions
<Image src="/photo.jpg" width={800} height={600} alt="Photo" />
// For responsive images, use fill with a sized container
<div style={{ position: 'relative', width: '100%', aspectRatio: '16/9' }}>
<Image src="/photo.jpg" fill alt="Photo" />
</div>
<!-- Nuxt - Same principle -->
<NuxtImg src="/photo.jpg" width="800" height="600" alt="Photo" />
<!-- Responsive with aspect ratio -->
<div class="image-container">
<NuxtImg src="/photo.jpg" fit="cover" alt="Photo" />
</div>
<style>
.image-container {
position: relative;
width: 100%;
aspect-ratio: 16 / 9;
}
</style>
Framework-specific CLS gotchas:
| Issue | Next.js Solution | Nuxt Solution |
|---|---|---|
| Font loading | Use next/font with display: swap | Use @nuxtjs/fontaine |
| Dynamic content | Use Suspense with sized fallbacks | Use v-if with sized placeholders |
| Ads/embeds | Reserve space with CSS | Reserve space with CSS |
INP optimization
INP measures how quickly your app responds to user interactions. This is where React and Vue have different patterns.
Next.js (React) approach:
React 18+ provides useTransition for non-urgent updates:
'use client';
import { useState, useTransition } from 'react';
function SearchResults({ items }) {
const [query, setQuery] = useState('');
const [filtered, setFiltered] = useState(items);
const [isPending, startTransition] = useTransition();
function handleSearch(value) {
// Input updates immediately (urgent)
setQuery(value);
// Filtering happens in background (non-urgent)
startTransition(() => {
setFiltered(items.filter(i => i.name.includes(value)));
});
}
return (
<div>
<input
value={query}
onChange={e => handleSearch(e.target.value)}
/>
<div style={{ opacity: isPending ? 0.7 : 1 }}>
{filtered.map(item => <Item key={item.id} {...item} />)}
</div>
</div>
);
}
Nuxt (Vue) approach:
Vue doesn't have useTransition, but you can achieve similar results with debouncing and nextTick:
<template>
<div>
<input v-model="query" @input="handleSearch" />
<div :style="{ opacity: isPending ? 0.7 : 1 }">
<Item v-for="item in filtered" :key="item.id" v-bind="item" />
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
import { useDebounceFn } from '@vueuse/core';
const query = ref('');
const debouncedQuery = ref('');
const isPending = ref(false);
const items = defineProps(['items']);
const filtered = computed(() =>
items.filter(i => i.name.includes(debouncedQuery.value))
);
const handleSearch = useDebounceFn(() => {
isPending.value = true;
// Use requestAnimationFrame for visual feedback
requestAnimationFrame(() => {
debouncedQuery.value = query.value;
isPending.value = false;
});
}, 150);
</script>
Which is better for INP?
React's useTransition is more elegant and integrated into the framework. Vue's approach requires external utilities but offers more explicit control. In practice, both achieve similar INP scores when implemented correctly.
The verdict
Both frameworks can achieve excellent Core Web Vitals scores. The differences are in ergonomics, not capability:
- Next.js advantages: Built-in image optimization, native
useTransitionfor INP, streaming SSR out of the box - Nuxt advantages: Smaller baseline bundle, simpler data fetching patterns, font optimization via modules
Framework choice doesn't determine your scores - implementation does. A well-optimized Nuxt site will outperform a poorly optimized Next.js site, and vice versa.
Decision guide
Choose Next.js if:
- Your team knows React
- You're deploying to Vercel
- You need mature Server Component patterns
- You're building a large-scale app with complex data requirements
- You want built-in image optimization without setup
Choose Nuxt if:
- Your team knows Vue
- You need multi-cloud deployment flexibility
- You're building content-driven sites
- You want smaller bundles out of the box
- Edge cold start time is critical
Either works well for:
- E-commerce sites
- Marketing sites
- Dashboards
- Content management
- API-driven applications
Quick comparison table
| Aspect | Next.js | Nuxt 3 |
|---|---|---|
| Base framework | React | Vue |
| SSR throughput (simple) | Faster (+87%) | Slower |
| SSR throughput (API fetch) | Slower | Faster (+144%) |
| Bundle size (baseline) | Larger | Smaller (~33% less) |
| Server Components | Mature, default | Experimental |
| Image optimization | Built-in | Requires module |
| Build tool | Turbopack (Rust) | Vite |
| Edge cold start | ~10-30ms | ~2ms |
| Deployment | Best on Vercel | Flexible (Nitro) |
| Core Web Vitals | Excellent | Excellent |
The bottom line
Pick your framework based on your team's expertise and deployment needs—not performance benchmarks. Both Next.js and Nuxt can achieve sub-2-second LCP and passing Core Web Vitals when configured properly.
What actually moves the needle:
- Image optimization — biggest LCP impact, easiest win
- Code splitting — lazy load what users don't need immediately
- Server rendering — faster TTFB, better SEO
- Reserved space — prevent layout shifts with explicit dimensions
Framework-specific guides:
Already built your app and wondering where you're losing points? Run it through PageSpeedFix to identify exactly what's hurting your Core Web Vitals—we'll tell you whether it's framework-related or not.