Turbopack Incremental Compilation

Turbopack replaces monolithic rebuild cycles with a Rust-native demand-driven computation engine: instead of re-traversing entry points on every file change, it models the project as a graph of memoized functions and recomputes only the nodes a change actually invalidates. By isolating changed nodes, computing minimal deltas, and persisting serialized results across sessions, it reaches sub-50ms hot module replacement (HMR) and high cache reuse on warm starts. This guide details the graph engine, delta propagation, and invalidation strategy needed to run Turbopack in real Next.js projects; for the broader engine comparison and pipeline orchestration, start from esbuild & Turbopack Workflows before tuning the knobs below.

Turbopack ships as the development server engine inside Next.js. Enable it with next dev --turbopack (the --turbopack flag is stable as of Next.js 15). Standalone Turbopack outside Next.js is not yet stable for production.

Turbopack demand-driven incremental computation A request walks the memoized function graph; unchanged nodes are reused from cache and only invalidated nodes are recomputed. Request a route, recompute only invalidated nodes Request app/page.tsx parse() node cache hit, reuse resolve() node cache hit, reuse transform() node invalidated, recompute HMR delta < 50KB patch Persistent cache: .next/cache/turbopack serialized node results keyed by content hash + compiler flags + env digest, restored on warm start
Figure: a request walks the memoized function graph; cached nodes are reused and only the invalidated transform is recomputed and shipped as an HMR delta.

Prerequisites

This guide assumes Next.js 15.x, Node 18.18+ (Node 20 LTS recommended), and a project using the App Router. Verify your toolchain before configuring anything:

# Next.js 15.x / Node 20+
node --version          # v20.x or newer
npx next --version      # Next.js 15.x
cat package.json | grep '"next"'

Pin Next.js exactly in CI. Turbopack’s on-disk cache format is tied to the engine version baked into each Next.js release; a minor bump can invalidate every persisted node, so an unpinned dependency turns warm starts into silent cold starts.

Core Mechanics

Demand-driven graph tracking

Turbopack constructs a persistent computation graph during the initial parse phase. Each unit of work — parse, resolve, transform — is a memoized function whose output is cached against its inputs. Imports resolve at the AST level and map to immutable module identifiers. When a source file mutates, the engine marks the dirty function nodes and recomputes only the affected subgraph, walking outward until a node’s output is unchanged and propagation stops. This is the foundation of every incremental claim Turbopack makes; the same demand-driven model underpins the engine comparison in esbuild & Turbopack Workflows.

Partial module evaluation

Only modules with changed AST nodes or updated dependency hashes are re-evaluated. The Rust execution engine isolates evaluation contexts so unchanged modules retain their compiled output. In benchmarked App Router projects, this drops incremental latency from roughly 800ms (full rebuild) to under 35ms for a localized edit in a tree exceeding 5,000 modules.

State persistence across sessions

Graph state survives process restarts through serialized snapshots written under .next/cache/turbopack. The engine persists module boundaries, hash digests, and node results, so a warm start restores the graph instead of re-parsing the dependency tree. The detailed on-disk layout and CI restoration strategy live in Configuring Turbopack cache for Next.js projects.

Configuration & CLI Reference

Turbopack configuration lives under the top-level turbopack key in next.config.js as of Next.js 15.3 (the older experimental.turbo namespace is deprecated and emits a warning). The block below is complete and runnable.

// next.config.js — Next.js 15.3.x / Node 20+
/** @type {import('next').NextConfig} */
const nextConfig = {
  turbopack: {
    // Map non-standard extensions to webpack-compatible loaders.
    rules: {
      '*.svg': {
        loaders: ['@svgr/webpack'],
        as: '*.js',
      },
    },
    // Override module resolution aliases (mirror your tsconfig paths).
    resolveAlias: {
      '@/components': './src/components',
      '@/lib': './src/lib',
    },
    // Resolve order. Defaults: .tsx .ts .jsx .js .mjs .cjs .json
    resolveExtensions: ['.tsx', '.ts', '.jsx', '.js', '.mjs', '.json'],
  },
};

module.exports = nextConfig;
# Next.js 15.3.x — start, trace, and reset
next dev --turbopack                          # enable the dev engine
NEXT_TURBOPACK_TRACING=1 next dev --turbopack  # verbose timing trace
rm -rf .next && next dev --turbopack           # discard all cache, full rebuild

Step-by-Step Workflow

  1. Enable the engine. Switch your dev script to next dev --turbopack and start it once to populate the cache.
  2. Confirm the cache materialized. Run ls -lh .next/cache/turbopack — a populated directory is the prerequisite for any warm-start gain.
  3. Measure a warm start. Stop the server, restart with next dev --turbopack, and compare ready-time against the first run; a warm start should be markedly faster.
  4. Edit a leaf component and watch the terminal for Compiled in <N>ms. A single-file edit should recompile in well under 200ms.
  5. Trace a slow rebuild. If a rebuild exceeds 200ms, run NEXT_TURBOPACK_TRACING=1 next dev --turbopack and inspect which node dominates the recompute.

Verify the delta path directly: open the browser DevTools Network tab, filter to WebSocket frames, and confirm a single-file edit ships a payload under ~50KB rather than a full bundle.

Debugging & Failure Modes

Symlinked directories and dynamic import() with variable paths can make the watcher miss filesystem events, leaving stale results. Prefer static string literals or explicit glob patterns for dynamic imports. For symlinked packages in a monorepo, declare them as workspace dependencies in package.json so the resolver tracks them. Resolution failures from these same patterns are diagnosed in depth in Debugging Turbopack module resolution errors.

Non-deterministic loaders

Webpack-compatible loaders registered under turbopack.rules must produce identical output for identical input. A loader that embeds a timestamp or random id breaks the memoization contract and forces perpetual recompilation, defeating the cache.

Environment-variable drift

Environment variables are folded into node cache keys. Editing .env.local invalidates affected boundaries, and the running server may hold a stale value. Restart the dev server after any env change to guarantee a consistent graph.

Forced rebuild

When HMR stops reflecting changes or the cache appears corrupt:

# Most reliable reset
rm -rf .next && next dev --turbopack

# Bisect against the webpack bundler to isolate a Turbopack-specific bug
next dev

If a page reflects edits under next dev (webpack) but not under --turbopack, capture NEXT_TURBOPACK_TRACING=1 output and file it against Next.js with reproduction steps.

Performance & Measurement

Metric Cold start (no cache) Warm start (restored cache)
Initial ready time full graph build restored snapshot, markedly faster
Single-file HMR n/a < 50ms typical
HMR payload n/a < 50KB per leaf edit
Recompiled nodes entire reachable graph invalidated subgraph only

Measure ready time from the terminal banner, recompile latency from Compiled in <N>ms, and payload size from WebSocket frames in DevTools. Treat any single-file rebuild over 200ms as a module-boundary smell worth tracing.

Compatibility Matrix

Next.js Config key Node Notes
13.x experimental.turbo 16.14+ Alpha dev engine, frequent cache breakage
14.x experimental.turbo 18.17+ Dev engine stabilizing
15.0–15.2 experimental.turbo 18.18+ --turbopack flag stable
15.3+ turbopack (top-level) 18.18+ experimental.turbo deprecated, warns

In-Depth Guides