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:

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:

Why Rust?

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:

Performance Claims and Controversy:

When Vercel announced Turbopack, they claimed it was "10x faster than Vite."

Evan You (Vite creator) investigated. His findings:

Current Status (2025):


How Rendering Actually Works

Server-Side Rendering (SSR) Pipeline

  1. Request arrives at the Next.js server
  2. Route matching — Which page handles this URL?
  3. Data fetching — Execute getServerSideProps (Pages) or async component (App)
  4. React rendering — Render components to HTML string
  5. HTML response — Send HTML to browser
  6. Client hydration — React takes over the static HTML

Static Generation (SSG) Pipeline

  1. Build time — Next.js identifies all static routes
  2. Data fetching — Execute getStaticProps for each page
  3. React rendering — Render components to HTML files
  4. Deploy — HTML files served directly from CDN
  5. Request time — No server rendering, just file serving
  6. Client hydration — React takes over

Streaming and Suspense

Traditional SSR sends the complete HTML at once. Streaming sends HTML progressively:

  1. Request arrives
  2. Send initial HTML — Layout, navigation, loading states
  3. Data fetching — Components fetch their data
  4. Stream content — As each component finishes, stream its HTML
  5. 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:

  1. Next.js renders the page
  2. When it encounters a Suspense boundary around dynamic content, it stops
  3. The static parts become the "shell"
  4. The dynamic parts become "holes" to be filled later

Request Time:

  1. User requests the page
  2. Static shell is served immediately (from CDN if possible)
  3. Dynamic holes are computed and streamed in
  4. 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:

When detected, it triggers the nearest Suspense boundary's fallback.

The Technical Magic:

PPR leverages:

"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

  1. At build time, Next.js identifies functions with 'use server'
  2. It generates a unique ID for each action
  3. On the client, the form submission becomes a POST request to a special endpoint
  4. The server deserializes the form data and calls the function
  5. 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:

Edge Runtime:

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


Sources