Under The Hood: How Next.js Actually Works
"Turbopack is a continuation of the trend that began a few releases ago, where we started replacing Babel with SWC. Now we're doing that again, but for the entirety of the compiler and bundler." — Guillermo Rauch
This chapter goes beneath the API to understand the machinery that powers Next.js.
The Build System Evolution
The Webpack Era (2016-2022)
Next.js originally used Webpack as its bundler. Webpack:
- Bundles JavaScript modules
- Handles CSS, images, and other assets
- Performs code splitting
- Enables hot module replacement (HMR)
The Problem: Webpack is written in JavaScript. As projects grew larger, build times became painful. A large project might take 30+ seconds to start the dev server.
SWC: The Rust Compiler (2021)
Next.js 12 introduced SWC (Speedy Web Compiler) to replace Babel.
What SWC Does:
- Transpiles modern JavaScript/TypeScript to browser-compatible code
- Handles JSX transformation
- Performs minification
Why Rust?
- Rust is compiled to native code, not interpreted JavaScript
- SWC claims to be 17x faster than Babel
- Memory safety without garbage collection pauses
The Impact: Compilation became dramatically faster, even while Webpack remained the bundler.
Turbopack: The Webpack Successor (2022+)
Next.js 13 introduced Turbopack, a Rust-based bundler designed to replace Webpack entirely.
What Turbopack Does:
- Bundles JavaScript and TypeScript
- Uses SWC for compilation
- Incremental computation (only rebuilds what changed)
- Native support for React Server Components
Performance Claims and Controversy:
When Vercel announced Turbopack, they claimed it was "10x faster than Vite."
Evan You (Vite creator) investigated. His findings:
- The Vite implementation in Vercel's benchmark used the default Babel-based React plugin, while Turbopack used SWC
- When measuring hmr_to_commit (a fairer metric), the advantage dropped to below 2x
- Startup time comparisons were more favorable to Turbopack: 1.8 seconds vs. Vite's 11.4 seconds on a 3,000-module app
Current Status (2025):
- Turbopack is stable for development in Next.js 15+
- Turbopack is stable for production builds in Next.js 16+
- 100% integration test compatibility with Webpack
How Rendering Actually Works
Server-Side Rendering (SSR) Pipeline
- Request arrives at the Next.js server
- Route matching — Which page handles this URL?
- Data fetching — Execute
getServerSideProps(Pages) or async component (App) - React rendering — Render components to HTML string
- HTML response — Send HTML to browser
- Client hydration — React takes over the static HTML
Static Generation (SSG) Pipeline
- Build time — Next.js identifies all static routes
- Data fetching — Execute
getStaticPropsfor each page - React rendering — Render components to HTML files
- Deploy — HTML files served directly from CDN
- Request time — No server rendering, just file serving
- Client hydration — React takes over
Streaming and Suspense
Traditional SSR sends the complete HTML at once. Streaming sends HTML progressively:
- Request arrives
- Send initial HTML — Layout, navigation, loading states
- Data fetching — Components fetch their data
- Stream content — As each component finishes, stream its HTML
- Client hydration — Happens progressively as HTML arrives
The Technical Mechanism:
React uses renderToPipeableStream (Node.js) or renderToReadableStream (Edge) to generate HTML as a stream. Suspense boundaries define where to show fallbacks.
<Suspense fallback={<Loading />}>
<SlowComponent /> {/* Streams in when ready */}
</Suspense>
Partial Pre-Rendering (PPR) Deep Dive
PPR is the most sophisticated rendering technique in Next.js 16.
How PPR Works
Build Time:
- Next.js renders the page
- When it encounters a Suspense boundary around dynamic content, it stops
- The static parts become the "shell"
- The dynamic parts become "holes" to be filled later
Request Time:
- User requests the page
- Static shell is served immediately (from CDN if possible)
- Dynamic holes are computed and streamed in
- All content arrives in a single HTTP response
The Detection Mechanism
"The problem the team spent the past year working on is how to detect when you try to access request data."
Next.js detects dynamic behavior by watching for:
cookies()accessheaders()accesssearchParamsaccess- Other request-specific data
When detected, it triggers the nearest Suspense boundary's fallback.
The Technical Magic:
PPR leverages:
- Promises and the Node.js event loop
- React's Suspense architecture
- React Server Components' streaming capabilities
"Suspense is our mortar, allowing us to create stable boundaries for the dynamic parts of the page to be streamed in."
The Data Layer
Fetch with Caching (App Router)
Next.js extends the native fetch API with caching and revalidation:
// Cached indefinitely (force-cache is the default in v14, opt-in in v15+)
const data = await fetch(url, { cache: 'force-cache' });
// Never cached
const data = await fetch(url, { cache: 'no-store' });
// Revalidate after 60 seconds
const data = await fetch(url, { next: { revalidate: 60 } });
// Tag-based revalidation
const data = await fetch(url, { next: { tags: ['products'] } });
How Caching Works
Request Memoization:
If the same fetch is called multiple times during a single render, it's deduplicated. Only one request goes out.
Data Cache: Fetch results can be stored in a persistent cache. The cache key is the URL + options.
Full Route Cache: For static routes, the entire rendered HTML is cached.
Router Cache: Client-side, Next.js caches route data to make navigation instant.
use cache (Next.js 16+)
The new explicit caching directive:
'use cache';
async function CachedComponent() {
const data = await expensiveOperation();
return <div>{data}</div>;
}
This opts the component into caching. The component's output is cached and reused.
Server Actions
How Server Actions Work
Server Actions are functions that run on the server but can be called from the client.
// In a Server Component
async function submitForm(formData) {
'use server';
await db.insert(formData);
}
// In the form
<form action={submitForm}>
<input name="email" />
<button type="submit">Submit</button>
</form>
Under The Hood
- At build time, Next.js identifies functions with
'use server' - It generates a unique ID for each action
- On the client, the form submission becomes a POST request to a special endpoint
- The server deserializes the form data and calls the function
- The result is serialized and sent back
This is how React and Next.js implement RPC (Remote Procedure Call) without you writing API routes.
File System Conventions
How Routing Works
The App Router uses file system conventions to define routes:
app/
├── page.js → /
├── about/page.js → /about
├── blog/[slug]/page.js → /blog/:slug
├── [...catchAll]/page.js → /:catchAll*
└── (group)/page.js → / (groups don't affect URL)
Special Files
| File | Purpose |
|---|---|
page.js |
The UI for a route |
layout.js |
Shared layout (persists across navigations) |
template.js |
Like layout, but re-renders on navigation |
loading.js |
Loading UI (shown while page loads) |
error.js |
Error UI (error boundary) |
not-found.js |
404 UI |
route.js |
API endpoint (instead of page.js) |
How Layouts Work
Layouts wrap their children and persist across navigations:
// app/layout.js
export default function RootLayout({ children }) {
return (
<html>
<body>
<nav>...</nav>
{children}
</body>
</html>
);
}
When navigating between pages, the layout doesn't re-render—only the {children} changes. This is why state in layouts persists.
The Edge vs. Node.js Runtime
Two Runtimes
Next.js can run on two different JavaScript runtimes:
Node.js Runtime:
- Full Node.js API access
- Can use any npm package
- Runs on traditional servers/containers
- Higher memory and CPU limits
Edge Runtime:
- Subset of Web APIs
- Runs on edge networks (close to users)
- Very fast cold starts
- Limited APIs and execution time
How to Choose
// Force Node.js runtime
export const runtime = 'nodejs';
// Force Edge runtime
export const runtime = 'edge';
As of Next.js 15.5, Middleware can use either runtime.
Key Takeaways
- SWC replaced Babel for compilation (17x faster)
- Turbopack is replacing Webpack for bundling (now stable in v16)
- Streaming uses React's
renderToPipeableStreamto send HTML progressively - PPR pre-renders static parts at build time, streams dynamic parts at request time
- Server Actions are RPC—functions that run on server, called from client
- The file system defines routes, layouts, loading states, and error boundaries
- Edge Runtime is fast but limited; Node.js Runtime is full-featured