next export vs next build: Understanding the Differences
A detailed comparison of next build and next export, when to use each, and how they affect deployment and performance
next export vs next build: Understanding the Differences
Introduction
For years, next export was the command you ran after next build to produce a fully static site. In Next.js 13+, next export was deprecated and replaced by the output: 'export' configuration option. Understanding the difference between a standard next build and a static export is critical for choosing the right deployment strategy.
After deploying applications to Vercel, AWS S3, Cloudflare Pages, and a custom nginx server, I have learned that the deployment target should dictate your build strategy, not the other way around.
Environment
- OS: Windows 11
- Node.js: v20.10.0
- Next.js: 14.0.4
- Deployment targets: Vercel, S3+CloudFront, Cloudflare Pages
Problem
The confusion stems from incomplete documentation and legacy tutorials:
# This command no longer exists in Next.js 13+
npx next export
# Instead, configure in next.config.js
module.exports = {
output: 'export',
}Even after switching to the configuration-based approach, developers encounter:
Error: Image Optimization is not compatible with `output: 'export'`.
Please use a custom loader or remove the images configuration.Error: Server Actions are not supported with static export.Error: Middleware cannot be used with static export.Analysis
The fundamental difference is:
next build (default): Produces a hybrid output that includes both static pages and a Node.js server. The server handles ISR, middleware, API routes, Server Actions, and dynamic routes. Deployment requires a Node.js runtime.
output: 'export' (static export): Produces a fully static site with no server. All pages must be pre-renderable at build time. No ISR, no middleware, no API routes, no Server Actions. Can be deployed to any static hosting.
Solution
Configuration for standard server build (Vercel, custom server):
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
// No output option - defaults to server mode
images: {
remotePatterns: [
{ protocol: 'https', hostname: '**.example.com' },
],
},
}
module.exports = nextConfig// package.json
{
"scripts": {
"build": "next build",
"start": "next start -p 3000"
}
}Configuration for static export (S3, Cloudflare Pages):
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'export',
images: {
// Required: disable image optimization or use custom loader
unoptimized: true,
// Or use a custom loader
loader: 'custom',
loaderFile: './lib/imageLoader.ts',
},
trailingSlash: true, // Generates /about/index.html instead of /about.html
}
module.exports = nextConfig// lib/imageLoader.ts
export default function imageLoader({
src,
width,
quality,
}: {
src: string
width: number
quality?: number
}) {
return `${src}?w=${width}&q=${quality || 75}`
}Comparison table:
Feature | next build (server) | output: 'export'
---------------------------|--------------------|-----------------
Static pages | Yes | Yes
Dynamic routes | Yes (SSR/ISR) | Partial (build time only)
ISR | Yes | No
Middleware | Yes | No
API Routes | Yes | No
Server Actions | Yes | No
Image optimization | Yes | No (or custom loader)
Runtime | Node.js | Static files only
Deployment | Any Node host | Any static host
Bundle size | Includes server | Client onlyAdapting code for static export:
// lib/compat.ts
export const isStaticExport = process.env.NEXT_PUBLIC_OUTPUT === 'export'
// Use this to conditionally disable features
export function checkFeatureAvailability(feature: string) {
if (isStaticExport && ['isr', 'middleware', 'api-routes'].includes(feature)) {
console.warn(`${feature} is not available in static export mode`)
return false
}
return true
}// app/posts/[slug]/page.tsx
// For static export, all slugs must be known at build time
export async function generateStaticParams() {
const posts = await fetch('https://api.example.com/posts').then((r) =>
r.json()
)
return posts.map((post: { slug: string }) => ({ slug: post.slug }))
}
// For dynamic params in static export, set to false
export const dynamicParams = falseLessons Learned
Choose your deployment target first. If you need ISR, middleware, or API routes, you must use server mode. Static export is for truly static sites.
Static export is not inferior. For documentation sites, blogs, and marketing pages, static export is faster, cheaper, and more reliable than a Node.js server.
Use
generateStaticParamsfor static export. Every dynamic route must have all its params generated at build time. There is no fallback.Cloudflare Pages supports static export natively. Deploy with
wrangler pages deploy outfor global edge distribution.Vercel handles both modes. If deploying to Vercel, there is no reason to use static export unless you specifically want the simplicity of static files.
This blog does not accept any external sponsorships, affiliate marketing, or ad revenue.