What Actually Happens During a Next.js Production Build
What Actually Happens During a Next.js Production Build
Running next build triggers a cascade of transformations that turn your source code into optimized production artifacts. Understanding this process isn't academic—it explains why certain patterns cause bundle bloat, why some imports break server components, and how to diagnose build performance issues.
This guide traces the complete build pipeline from source to deployment artifacts.
Build Pipeline Overview
┌─────────────────────────────────────────────────────────────────────────┐
│ NEXT.JS BUILD PIPELINE │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ Source Code │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Phase 1: ANALYSIS │ │
│ │ ├─ Route discovery (app/, pages/) │ │
│ │ ├─ Dependency graph construction │ │
│ │ ├─ Server/Client component classification │ │
│ │ └─ Static vs dynamic route detection │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Phase 2: TRANSFORMATION │ │
│ │ ├─ SWC parsing → AST │ │
│ │ ├─ TypeScript → JavaScript │ │
│ │ ├─ JSX → React.createElement / jsx-runtime │ │
│ │ ├─ Server Component transforms │ │
│ │ └─ next/dynamic, next/image transforms │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Phase 3: BUNDLING │ │
│ │ ├─ Webpack/Turbopack compilation │ │
│ │ ├─ Tree shaking (dead code elimination) │ │
│ │ ├─ Code splitting (chunks) │ │
│ │ ├─ CSS extraction and optimization │ │
│ │ └─ Asset hashing │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Phase 4: OPTIMIZATION │ │
│ │ ├─ Minification (SWC minifier) │ │
│ │ ├─ Chunk optimization (common chunks) │ │
│ │ ├─ Static page generation (SSG) │ │
│ │ └─ Route manifest generation │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Phase 5: OUTPUT │ │
│ │ ├─ .next/static/ (client bundles) │ │
│ │ ├─ .next/server/ (server bundles, RSC payloads) │ │
│ │ ├─ .next/cache/ (build cache) │ │
│ │ └─ Manifests (build-manifest, routes-manifest, etc.) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Phase 1: Route Discovery and Analysis
File-System Routing Resolution
Next.js scans your filesystem to discover routes:
// Simplified route discovery (internal Next.js logic)
interface RouteDefinition {
pathname: string;
type: 'page' | 'layout' | 'loading' | 'error' | 'not-found' | 'route';
isApi: boolean;
isDynamic: boolean;
segments: RouteSegment[];
}
interface RouteSegment {
name: string;
type: 'static' | 'dynamic' | 'catch-all' | 'optional-catch-all';
param?: string;
}
function discoverRoutes(appDir: string): RouteDefinition[] {
const routes: RouteDefinition[] = [];
function walkDirectory(dir: string, pathSegments: string[] = []) {
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory()) {
// Parse segment type
const segment = parseSegment(entry.name);
walkDirectory(
path.join(dir, entry.name),
[...pathSegments, entry.name]
);
} else if (isRouteFile(entry.name)) {
routes.push(buildRouteDefinition(pathSegments, entry.name));
}
}
}
walkDirectory(appDir);
return routes;
}
function parseSegment(name: string): RouteSegment {
// [slug] → dynamic
if (name.startsWith('[') && name.endsWith(']')) {
const inner = name.slice(1, -1);
// [[...slug]] → optional catch-all
if (inner.startsWith('[...') && inner.endsWith(']')) {
return {
name,
type: 'optional-catch-all',
param: inner.slice(4, -1),
};
}
// [...slug] → catch-all
if (inner.startsWith('...')) {
return { name, type: 'catch-all', param: inner.slice(3) };
}
// [slug] → dynamic
return { name, type: 'dynamic', param: inner };
}
// (group) → route group (doesn't affect URL)
if (name.startsWith('(') && name.endsWith(')')) {
return { name, type: 'static' }; // Groups don't create segments
}
// @parallel → parallel route
if (name.startsWith('@')) {
return { name, type: 'static' };
}
return { name, type: 'static' };
}
function isRouteFile(filename: string): boolean {
const routeFiles = [
'page.tsx', 'page.ts', 'page.jsx', 'page.js',
'layout.tsx', 'layout.ts', 'layout.jsx', 'layout.js',
'loading.tsx', 'loading.ts', 'loading.jsx', 'loading.js',
'error.tsx', 'error.ts', 'error.jsx', 'error.js',
'not-found.tsx', 'not-found.ts', 'not-found.jsx', 'not-found.js',
'route.tsx', 'route.ts', 'route.jsx', 'route.js',
'template.tsx', 'template.ts', 'template.jsx', 'template.js',
'default.tsx', 'default.ts', 'default.jsx', 'default.js',
];
return routeFiles.includes(filename);
}
Server/Client Component Classification
The build determines which components run where:
┌─────────────────────────────────────────────────────────────────────┐
│ COMPONENT BOUNDARY DETECTION │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Default: SERVER COMPONENT │
│ ═══════════════════════════ │
│ • Can use async/await directly │
│ • Can access backend resources │
│ • Cannot use hooks (useState, useEffect) │
│ • Cannot use browser APIs │
│ │
│ "use client" directive: CLIENT COMPONENT │
│ ═════════════════════════════════════════ │
│ • Boundary marker (file level) │
│ • Everything imported becomes client │
│ • Can use hooks and browser APIs │
│ • Serialized props only from server │
│ │
│ Detection Algorithm: │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ 1. Parse file for "use client" at top │ │
│ │ 2. If present → mark as client boundary │ │
│ │ 3. Trace imports from client boundary │ │
│ │ 4. All transitively imported = client │ │
│ │ 5. Everything else = server │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
│ Example: │
│ │
│ ServerPage.tsx (server) │
│ │ │
│ ├── imports Header.tsx (server) ✓ │
│ │ │ │
│ │ └── imports Logo.tsx (server) ✓ │
│ │ │
│ └── imports Counter.tsx ("use client") │
│ │ │
│ ├── imports useState (client) ✓ │
│ │ │
│ └── imports utils.ts (now client!) │
│ │ │
│ └── Tree-shaken differently │
│ │
└─────────────────────────────────────────────────────────────────────┘
Static vs Dynamic Detection
Next.js analyzes each route to determine rendering strategy:
// Simplified static/dynamic detection
type RenderingStrategy = 'static' | 'dynamic' | 'isr';
interface RouteAnalysis {
strategy: RenderingStrategy;
reason: string;
revalidate?: number | false;
}
function analyzeRoute(routePath: string, ast: AST): RouteAnalysis {
const dynamicIndicators = {
// Functions that force dynamic rendering
dynamicFunctions: ['cookies', 'headers', 'searchParams', 'useSearchParams'],
// Route segment config
dynamicConfig: ast.hasExport('dynamic', 'force-dynamic'),
// Uncached fetch calls
uncachedFetch: ast.hasFetch({ cache: 'no-store' }),
// Dynamic route without generateStaticParams
dynamicSegmentWithoutParams:
routePath.includes('[') && !ast.hasExport('generateStaticParams'),
};
// Check for ISR
if (ast.hasExport('revalidate')) {
const revalidateValue = ast.getExportValue('revalidate');
if (typeof revalidateValue === 'number') {
return {
strategy: 'isr',
reason: `revalidate = ${revalidateValue}`,
revalidate: revalidateValue,
};
}
}
// Check for static generation helpers
if (ast.hasExport('generateStaticParams')) {
return {
strategy: 'static',
reason: 'generateStaticParams provided',
};
}
// Check dynamic indicators
for (const [indicator, present] of Object.entries(dynamicIndicators)) {
if (present) {
return {
strategy: 'dynamic',
reason: indicator,
};
}
}
// Default to static
return {
strategy: 'static',
reason: 'No dynamic indicators found',
};
}
Phase 2: AST Transformation
SWC Parsing
Next.js uses SWC (Speedy Web Compiler) written in Rust for parsing and transformation:
┌─────────────────────────────────────────────────────────────────────┐
│ SWC TRANSFORMATION PIPELINE │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Source Code (TypeScript/JSX) │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ LEXER │ Tokenization │
│ │ (Rust) │ "const x = <div>" → tokens │
│ └────────┬────────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ PARSER │ Token stream → AST │
│ │ (Rust) │ Handles TS, JSX, ESNext │
│ └────────┬────────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ TRANSFORMS │ AST → AST (multiple passes) │
│ │ (Rust/WASM) │ │
│ │ ├─ TypeScript │ Strip types │
│ │ ├─ JSX │ → React.createElement │
│ │ ├─ Decorators │ → helper calls │
│ │ ├─ async/await │ → generators (if needed) │
│ │ └─ Next.js │ Custom transforms │
│ └────────┬────────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ CODE GEN │ AST → JavaScript │
│ │ (Rust) │ + Source maps │
│ └────────┬────────┘ │
│ ▼ │
│ Output (JavaScript + Sourcemap) │
│ │
└─────────────────────────────────────────────────────────────────────┘
Next.js-Specific Transforms
// Example: What happens to next/image during build
// INPUT: Your code
import Image from 'next/image';
export default function Hero() {
return (
<Image
src="/hero.jpg"
alt="Hero"
width={1200}
height={600}
priority
/>
);
}
// OUTPUT: After transformation
import { jsx as _jsx } from "react/jsx-runtime";
import { getImageProps as _getImageProps } from "next/image";
export default function Hero() {
return _jsx("img", {
..._getImageProps({
src: "/hero.jpg",
alt: "Hero",
width: 1200,
height: 600,
priority: true,
}).props,
// Additional optimizations injected
loading: "eager", // priority=true
fetchPriority: "high",
decoding: "async",
"data-nimg": "1",
style: {
color: "transparent",
},
srcSet: "/_next/image?url=%2Fhero.jpg&w=1200&q=75 1x, /_next/image?url=%2Fhero.jpg&w=2400&q=75 2x",
src: "/_next/image?url=%2Fhero.jpg&w=2400&q=75",
});
}
Server Component Transform
// INPUT: Server Component
// app/posts/[id]/page.tsx
import { db } from '@/lib/db';
export default async function PostPage({ params }: { params: { id: string } }) {
const post = await db.post.findUnique({ where: { id: params.id } });
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
);
}
// OUTPUT: Server Component with RSC serialization hooks
// The component stays on server, but Next.js wraps it with:
// 1. Flight serialization protocol
// 2. Streaming boundary markers
// 3. Client reference placeholders
// Server bundle (.next/server/app/posts/[id]/page.js):
const PostPage = async function({ params }) {
const post = await db.post.findUnique({ where: { id: params.id } });
return {
$$typeof: Symbol.for('react.element'),
type: 'article',
props: {
children: [
{ $$typeof: Symbol.for('react.element'), type: 'h1', props: { children: post.title } },
{ $$typeof: Symbol.for('react.element'), type: 'p', props: { children: post.content } },
],
},
};
};
// Client boundary reference (for Client Components imported by Server Components):
// { $$typeof: Symbol.for('react.client.reference'), $$id: 'app/components/LikeButton.tsx#default' }
Dynamic Import Transform
// INPUT: next/dynamic usage
import dynamic from 'next/dynamic';
const HeavyChart = dynamic(() => import('./Chart'), {
loading: () => <Skeleton />,
ssr: false,
});
// OUTPUT: Transformed to lazy loading with chunk
import { lazy, Suspense } from 'react';
// Creates separate chunk: chunks/Chart-[hash].js
const HeavyChart = lazy(() =>
import(/* webpackChunkName: "Chart" */ './Chart')
);
// Wrapped component with loading state
function HeavyChartWrapper(props) {
// SSR check injected
if (typeof window === 'undefined') {
return null; // ssr: false
}
return (
<Suspense fallback={<Skeleton />}>
<HeavyChart {...props} />
</Suspense>
);
}
Phase 3: Bundling with Webpack/Turbopack
Entry Point Generation
Next.js generates multiple entry points:
// Simplified entry point generation
interface EntryPoints {
// Client entries (one per route + shared)
client: {
main: string; // Main client runtime
webpack: string; // Webpack runtime
[route: string]: string; // Per-route client bundles
};
// Server entries
server: {
[route: string]: string; // Per-route server bundles
middleware?: string; // Middleware bundle
};
// Edge entries
edge: {
[route: string]: string; // Edge runtime routes
};
}
function generateEntryPoints(routes: RouteDefinition[]): EntryPoints {
const entries: EntryPoints = {
client: {
main: 'next/dist/client/next.js',
webpack: 'next/dist/client/webpack.js',
},
server: {},
edge: {},
};
for (const route of routes) {
const entryName = routeToEntryName(route.pathname);
// Server entry (always generated)
entries.server[entryName] = route.filepath;
// Client entry (for pages with client components)
if (hasClientComponents(route)) {
entries.client[entryName] = generateClientEntry(route);
}
// Edge entry (if route uses edge runtime)
if (route.runtime === 'edge') {
entries.edge[entryName] = route.filepath;
}
}
return entries;
}
Tree Shaking Deep Dive
Tree shaking eliminates dead code by analyzing the module graph:
┌─────────────────────────────────────────────────────────────────────┐
│ TREE SHAKING PROCESS │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 1. BUILD DEPENDENCY GRAPH │
│ ───────────────────────── │
│ │
│ entry.ts │
│ │ │
│ ├── import { used } from './utils' │
│ │ │
│ └── import { ComponentA } from './components' │
│ │
│ utils.ts │
│ ├── export function used() { ... } ← KEPT (imported) │
│ ├── export function unused() { ... } ← REMOVED │
│ └── export const CONSTANT = 42; ← REMOVED (not imported) │
│ │
│ 2. MARK PHASE (identify used exports) │
│ ───────────────────────────────────── │
│ │
│ For each import: │
│ • Mark the imported binding as "used" │
│ • Recursively mark dependencies of that binding │
│ • Handle re-exports (export { x } from './other') │
│ │
│ 3. SWEEP PHASE (remove unused) │
│ ────────────────────────────── │
│ │
│ • Remove unmarked exports │
│ • Remove side-effect-free modules with no used exports │
│ • Keep modules with side effects (or marked sideEffects: true) │
│ │
│ 4. MINIFICATION (DCE - Dead Code Elimination) │
│ ───────────────────────────────────────────── │
│ │
│ • Remove unreachable code branches │
│ • Inline constant values │
│ • Remove unused variables │
│ │
└─────────────────────────────────────────────────────────────────────┘
What Breaks Tree Shaking
// ❌ BARREL FILES - Kill tree shaking
// components/index.ts
export * from './Button';
export * from './Modal';
export * from './Table';
export * from './Chart'; // 200KB library
// Importing ONE component pulls in ALL of them
import { Button } from '@/components';
// Result: Chart's 200KB is bundled even though unused
// ✅ FIX: Direct imports
import { Button } from '@/components/Button';
// ❌ SIDE EFFECTS IN MODULE SCOPE
// analytics.ts
console.log('Analytics loaded'); // Side effect!
export function track() { ... }
export function identify() { ... }
// Even if nothing is imported, the console.log runs
// Webpack can't remove this module
// ✅ FIX: Mark as side-effect-free in package.json
{
"sideEffects": false
}
// Or be explicit:
{
"sideEffects": ["*.css", "./src/polyfills.ts"]
}
// ❌ DYNAMIC PROPERTY ACCESS
import * as utils from './utils';
const fn = utils[someVariable]; // Can't statically analyze
fn();
// ✅ FIX: Explicit imports
import { specificFn } from './utils';
specificFn();
// ❌ COMMONJS INTEROP
const lodash = require('lodash'); // CJS - no tree shaking
// ✅ FIX: Use ESM
import { debounce } from 'lodash-es';
// Or cherry-pick
import debounce from 'lodash/debounce';
Code Splitting Strategy
┌─────────────────────────────────────────────────────────────────────┐
│ CHUNK SPLITTING STRATEGY │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ CHUNK TYPES │
│ ═══════════ │
│ │
│ 1. FRAMEWORK CHUNK (framework-[hash].js) │
│ └── React, ReactDOM, Next.js runtime │
│ Cached across deployments, ~150KB │
│ │
│ 2. COMMONS CHUNK (commons-[hash].js) │
│ └── Shared dependencies used by >50% of pages │
│ Libraries like date-fns, lodash utilities │
│ │
│ 3. SHARED CHUNKS ([id]-[hash].js) │
│ └── Code shared between 2+ pages │
│ Split by usage patterns │
│ │
│ 4. PAGE CHUNKS (pages/[route]-[hash].js) │
│ └── Route-specific code │
│ Loaded on navigation │
│ │
│ 5. DYNAMIC CHUNKS (chunks/[name]-[hash].js) │
│ └── next/dynamic imports │
│ Loaded on demand │
│ │
│ SPLITTING ALGORITHM │
│ ═══════════════════ │
│ │
│ For each module M: │
│ ├── If M is React/Next.js core → framework chunk │
│ ├── If M used by >50% routes → commons chunk │
│ ├── If M used by 2+ routes → shared chunk (by usage group) │
│ ├── If M is dynamic import → separate chunk │
│ └── Otherwise → include in page chunk │
│ │
│ SIZE THRESHOLDS │
│ ════════════════ │
│ • minSize: 20KB (don't create tiny chunks) │
│ • maxSize: 244KB (keep chunks reasonable) │
│ • maxAsyncRequests: 30 (limit parallel loads) │
│ │
└─────────────────────────────────────────────────────────────────────┘
Webpack Configuration (Generated)
// Simplified version of what Next.js generates
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
// Framework chunk
framework: {
name: 'framework',
test: /[\\/]node_modules[\\/](react|react-dom|scheduler|next)[\\/]/,
priority: 40,
enforce: true,
},
// Library chunk (large dependencies)
lib: {
test: /[\\/]node_modules[\\/]/,
name(module) {
const match = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/);
return `npm.${match[1].replace('@', '')}`;
},
priority: 30,
minChunks: 1,
reuseExistingChunk: true,
},
// Commons chunk
commons: {
name: 'commons',
minChunks: 2, // Used by 2+ pages
priority: 20,
},
// Shared chunks by entry
shared: {
name(module, chunks) {
return `shared-${chunks.map(c => c.name).sort().join('-')}`;
},
priority: 10,
minChunks: 2,
reuseExistingChunk: true,
},
},
},
// Module concatenation (scope hoisting)
concatenateModules: true,
// Minification
minimize: true,
minimizer: [
// SWC minifier (faster than Terser)
new TerserPlugin({
minify: TerserPlugin.swcMinify,
terserOptions: {
compress: {
dead_code: true,
drop_console: process.env.NODE_ENV === 'production',
drop_debugger: true,
},
mangle: true,
},
}),
],
},
};
Phase 4: RSC Bundling
Flight Protocol
React Server Components use a custom serialization protocol:
┌─────────────────────────────────────────────────────────────────────┐
│ RSC FLIGHT PROTOCOL │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Server renders component tree → Flight payload (streaming) │
│ │
│ PAYLOAD FORMAT │
│ ══════════════ │
│ │
│ 0:["$","div",null,{"children":[["$","h1",null,{"children":"Hi"}]]}]
│ │ │ │ │ │ │
│ │ │ │ │ └── Props (serialized) │
│ │ │ │ └── Key │
│ │ │ └── Element type │
│ │ └── React element marker │
│ └── Row ID (for streaming) │
│ │
│ REFERENCE TYPES │
│ ═══════════════ │
│ │
│ "$" - React Element │
│ "@" - Promise (for Suspense) │
│ "S" - Symbol │
│ "F" - Server Reference (Server Actions) │
│ "M" - Client Reference (Client Component) │
│ │
│ CLIENT COMPONENT REFERENCE │
│ ═══════════════════════════ │
│ │
│ M1:{"id":"./app/components/Counter.tsx","name":"default","chunks":[...]}
│ │
│ Tells client: "Load this chunk, render this component here" │
│ │
│ STREAMING EXAMPLE │
│ ═════════════════ │
│ │
│ 0:["$","html",null,{"children":...}] ← Shell │
│ 1:["$","$L2",null,{}] ← Placeholder for async │
│ 2:@3 ← Promise reference │
│ 3:["$","article",null,{"children":...}] ← Resolved content │
│ │
└─────────────────────────────────────────────────────────────────────┘
Server/Client Bundle Separation
// Build generates separate bundles
// .next/server/app/page.js - Server Bundle
// Contains:
// - Server Components
// - Server-only code
// - Database queries
// - References to Client Components (not the code itself)
// .next/static/chunks/app/page-[hash].js - Client Bundle
// Contains:
// - Client Components ("use client")
// - Event handlers
// - Hooks (useState, useEffect)
// - Browser APIs
// Build process:
// 1. Parse all files, identify "use client" boundaries
// 2. For server bundle: include server components, replace client imports with references
// 3. For client bundle: include client components and their dependencies
// 4. Generate manifest mapping references to chunk URLs
Client Reference Manifest
// .next/server/client-reference-manifest.json
{
"ssrModuleMapping": {
"app/components/Counter.tsx": {
"default": {
"id": "app/components/Counter.tsx",
"name": "default",
"chunks": ["app/components/Counter-abc123.js"]
}
}
},
"clientModules": {
"app/components/Counter.tsx#default": {
"id": "app/components/Counter.tsx",
"name": "default",
"chunks": ["static/chunks/app/page-xyz789.js"],
"async": false
}
}
}
Phase 5: Route Segment Optimization
Partial Prerendering (PPR)
┌─────────────────────────────────────────────────────────────────────┐
│ PARTIAL PRERENDERING │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Build Time Analysis │
│ ═══════════════════ │
│ │
│ Page Structure: │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Layout (static) │ │
│ │ ┌──────────────────────────────────────────────────────┐ │ │
│ │ │ Header (static) ✓ Prerendered │ │ │
│ │ └──────────────────────────────────────────────────────┘ │ │
│ │ ┌──────────────────────────────────────────────────────┐ │ │
│ │ │ Main Content │ │ │
│ │ │ ┌────────────────────────────────────────────────┐ │ │ │
│ │ │ │ Static Section ✓ Prerendered │ │ │ │
│ │ │ └────────────────────────────────────────────────┘ │ │ │
│ │ │ ┌────────────────────────────────────────────────┐ │ │ │
│ │ │ │ <Suspense> │ │ │ │
│ │ │ │ <UserDashboard /> ✗ Dynamic (uses cookies) │ │ │ │
│ │ │ │ </Suspense> │ │ │ │
│ │ │ └────────────────────────────────────────────────┘ │ │ │
│ │ │ ┌────────────────────────────────────────────────┐ │ │ │
│ │ │ │ Footer (static) ✓ Prerendered │ │ │ │
│ │ │ └────────────────────────────────────────────────┘ │ │ │
│ │ └──────────────────────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ Build Output: │
│ • Static shell HTML (instant serving) │
│ • Fallback UI for dynamic sections │
│ • Server functions for dynamic content │
│ │
│ Runtime: │
│ 1. Serve static shell immediately │
│ 2. Stream dynamic content into Suspense boundaries │
│ 3. Hydrate progressively │
│ │
└─────────────────────────────────────────────────────────────────────┘
Static Generation with generateStaticParams
// app/posts/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await db.post.findMany({ select: { slug: true } });
return posts.map(post => ({ slug: post.slug }));
}
// Build process:
// 1. Call generateStaticParams() → [{ slug: 'hello' }, { slug: 'world' }]
// 2. For each params object:
// a. Render the page with those params
// b. Generate static HTML
// c. Generate RSC payload
// d. Store in .next/server/app/posts/[slug]/
// Output structure:
// .next/server/app/posts/[slug]/
// ├── hello.html # Static HTML
// ├── hello.rsc # RSC payload
// ├── world.html
// ├── world.rsc
// └── page.js # Server bundle (for dynamic fallback)
Route Manifests
// .next/routes-manifest.json
{
"version": 3,
"basePath": "",
"redirects": [],
"rewrites": [],
"headers": [],
"staticRoutes": [
{ "page": "/", "regex": "^/$" },
{ "page": "/about", "regex": "^/about$" }
],
"dynamicRoutes": [
{
"page": "/posts/[slug]",
"regex": "^/posts/([^/]+?)$",
"namedRegex": "^/posts/(?<slug>[^/]+?)$",
"routeKeys": { "slug": "slug" }
}
],
"dataRoutes": [
{ "page": "/", "dataRouteRegex": "^/_next/data/BUILD_ID/index.json$" }
]
}
// .next/prerender-manifest.json
{
"version": 3,
"routes": {
"/": { "dataRoute": "/_next/data/BUILD_ID/index.json" },
"/posts/hello": {
"dataRoute": "/_next/data/BUILD_ID/posts/hello.json",
"srcRoute": "/posts/[slug]"
}
},
"dynamicRoutes": {
"/posts/[slug]": {
"routeRegex": "^/posts/([^/]+?)$",
"fallback": false
}
}
}
Build Output Structure
.next/
├── cache/ # Build cache (speeds up rebuilds)
│ ├── webpack/ # Webpack cache
│ │ ├── client-development/
│ │ └── server-development/
│ ├── fetch-cache/ # fetch() response cache
│ └── images/ # Optimized images cache
│
├── server/
│ ├── app/ # App Router server bundles
│ │ ├── page.js # Route handlers
│ │ ├── page_client-reference-manifest.js
│ │ └── [route]/
│ │ ├── page.js
│ │ └── page.rsc # RSC payload (for static routes)
│ │
│ ├── pages/ # Pages Router (if used)
│ │
│ ├── chunks/ # Server-side chunks
│ │ └── [hash].js
│ │
│ ├── vendor-chunks/ # node_modules (server)
│ │
│ ├── middleware.js # Middleware bundle
│ ├── middleware-manifest.json
│ │
│ ├── app-paths-manifest.json # Route → file mapping
│ ├── server-reference-manifest.json # Server Actions
│ └── client-reference-manifest.json # Client Components
│
├── static/
│ ├── chunks/ # Client-side JS chunks
│ │ ├── framework-[hash].js # React + Next.js runtime
│ │ ├── main-[hash].js # Main client bundle
│ │ ├── webpack-[hash].js # Webpack runtime
│ │ ├── pages/ # Page-specific bundles
│ │ └── app/ # App Router bundles
│ │
│ ├── css/ # Extracted CSS
│ │ └── [hash].css
│ │
│ ├── media/ # Static assets (fonts, images)
│ │
│ └── BUILD_ID/ # Versioned static files
│
├── BUILD_ID # Unique build identifier
├── build-manifest.json # Client bundle manifest
├── react-loadable-manifest.json # Dynamic import manifest
├── routes-manifest.json # All routes
├── prerender-manifest.json # Static/ISR routes
└── required-server-files.json # Files needed at runtime
Build Performance Optimization
Analyzing Build Times
# Enable verbose build output
NEXT_DEBUG_BUILD=1 next build
# Generate build trace
next build --profile
# Analyze bundle
ANALYZE=true next build
# Requires: npm install @next/bundle-analyzer
next.config.js Optimizations
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
// Use SWC minifier (faster than Terser)
swcMinify: true,
// Disable source maps in production (faster build)
productionBrowserSourceMaps: false,
// Experimental features
experimental: {
// Turbopack (faster builds)
turbo: {
rules: {
'*.svg': {
loaders: ['@svgr/webpack'],
as: '*.js',
},
},
},
// Parallel routes compilation
parallelServerCompiles: true,
parallelServerBuildTraces: true,
// Optimize package imports (auto tree-shake)
optimizePackageImports: ['lucide-react', '@radix-ui/react-icons'],
},
// Reduce bundle size
modularizeImports: {
'lodash': {
transform: 'lodash/{{member}}',
},
'@mui/material': {
transform: '@mui/material/{{member}}',
},
'@mui/icons-material': {
transform: '@mui/icons-material/{{member}}',
},
},
// Webpack customization
webpack: (config, { isServer, dev }) => {
// Example: Ignore large optional dependencies
config.plugins.push(
new webpack.IgnorePlugin({
resourceRegExp: /^(aws-sdk|mock-aws-s3)$/,
})
);
return config;
},
};
module.exports = nextConfig;
Caching Strategies
# CI caching for faster builds
# GitHub Actions example
- name: Cache Next.js build
uses: actions/cache@v4
with:
path: |
~/.npm
${{ github.workspace }}/.next/cache
key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**/*.ts', '**/*.tsx') }}
restore-keys: |
${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-
${{ runner.os }}-nextjs-
# Vercel automatically caches:
# - node_modules
# - .next/cache
# - Build outputs between deployments
Debugging Build Issues
Common Problems and Solutions
// PROBLEM: "Module not found" in production but works in dev
// Cause 1: Case sensitivity
// macOS is case-insensitive, Linux (CI/production) is case-sensitive
import { Button } from './button'; // File is actually Button.tsx
// ✅ Fix: Match exact casing
// Cause 2: Missing dependency
// Check if package is in dependencies vs devDependencies
// Production builds don't install devDependencies
// Cause 3: Client/Server boundary
// Server component importing browser-only module
import { window } from 'something'; // window undefined on server
// ✅ Fix: Use dynamic import with ssr: false
// PROBLEM: Bundle size explosion
// Use bundle analyzer
// npm install @next/bundle-analyzer
// Add to next.config.js:
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
});
module.exports = withBundleAnalyzer(nextConfig);
// Run: ANALYZE=true npm run build
// Opens interactive treemap showing what's in each chunk
// PROBLEM: "use client" directive not working
// Cause: Directive must be at the very top
"use client" // ✅ First line
// Not after comments
// This is a comment
"use client" // ❌ Too late, ignored
// PROBLEM: Server Action not found
// Cause: Missing "use server" or incorrect export
// Server Actions must be:
// 1. Async functions
// 2. Marked with "use server" (file level or function level)
// 3. Exported (if in separate file)
"use server"
export async function myAction(formData: FormData) {
// ...
}
Build Logs to Understand
Route (app) Size First Load JS
┌ ○ / 5.23 kB 92.1 kB
├ ○ /about 1.42 kB 88.3 kB
├ ● /posts/[slug] 3.18 kB 90.1 kB
├ ├ /posts/hello
├ └ /posts/world
├ ƒ /api/users 0 B 0 B
└ ƒ /dashboard 8.92 kB 95.8 kB
○ (Static) prerendered as static content
● (SSG) prerendered as static HTML (uses generateStaticParams)
ƒ (Dynamic) server-rendered on demand
First Load JS = framework + page-specific JS
= ~87kB (framework) + page chunk
If First Load JS > 100kB, consider:
- Code splitting with next/dynamic
- Moving heavy deps to client components
- Lazy loading below-fold content
Production Checklist
Before Build
- Remove unused dependencies (
npx depcheck) - Check for barrel file imports (use direct imports)
- Verify
sideEffectsin package.json - Review dynamic imports for large components
- Ensure images use next/image
Build Configuration
-
swcMinify: trueenabled - Source maps disabled for production (unless debugging)
-
optimizePackageImportsfor large libraries - Bundle analyzer run to check sizes
Post-Build Verification
- Check route summary for unexpected dynamic routes
- Verify First Load JS is reasonable (<100kB target)
- Confirm static routes are marked ○ or ●
- Test critical paths work without JS (SSR output)
CI/CD
- Build cache configured
-
next buildin CI matches production - Build time monitored for regressions
Summary
A Next.js production build is a complex orchestration of:
- Analysis - Discovering routes, classifying components, detecting render strategies
- Transformation - SWC parsing, TypeScript compilation, JSX transformation, Next.js-specific transforms
- Bundling - Webpack/Turbopack compilation, tree shaking, code splitting
- RSC Processing - Server/client separation, Flight protocol serialization
- Optimization - Static generation, route manifests, asset hashing
Understanding this pipeline explains why certain patterns cause issues (barrel files killing tree shaking, dynamic imports creating extra chunks) and how to optimize build performance and output size.
The key insight: Next.js builds are about creating the right split between what runs on the server, what ships to the client, and what can be prerendered—and doing so with minimal JavaScript payload.
What did you think?