Why Turbopack replaced Webpack, incremental computation model, SWC transforms, turbopack config and filesystem cache, module graph anatomy, diagnosing OOM build failures, and pageExtensions for non-standard project layouts.
A-11 — Build Engine Internals: Turbopack, SWC, Bundle Analysis
Who this is for: Architects who want to understand what happens between
npm run buildand the deployable output — how Turbopack replaced Webpack, why SWC is faster than Babel, how the module graph is constructed, and how to diagnose and fix build performance problems that appear when a codebase scales.
Why the Build Stack Changed
For four years, Next.js used Webpack 5 as its bundler and Babel as its JavaScript transformer. Both were fine at small scale. Both had known limitations at large scale:
Webpack's problem: Webpack rebuilds the module graph on every save. In a large application, the graph might have 10,000 modules. Even incremental builds re-analyse significant portions of this graph. Teams with large codebases saw 30-60 second hot reload times.
Babel's problem: Babel is written in JavaScript and processes files one-at-a-time. TypeScript type stripping, JSX transformation, decorator transforms — Babel does each file sequentially. SWC does the same transformations in Rust, parallelised across all CPU cores, 10-100x faster.
The replacement strategy: SWC for JavaScript/TypeScript transformation (already the default in Next.js 12+), Turbopack for bundling (stable in Next.js 15 for development, nearing stability for production).
SWC — The Transformation Layer
SWC (Speedy Web Compiler) is a Rust-based JavaScript/TypeScript compiler. It replaced Babel as Next.js's transform layer in version 12.
What SWC handles:
- TypeScript → JavaScript (type stripping, not type checking)
- JSX → React.createElement calls
- ES2022+ → target ES version
- Import path transforms (absolute imports, module aliases)
- Emotion/styled-components transforms (when configured)
- React Compiler transforms (when enabled)
What SWC does not do:
- Type checking (that's
tsc --noEmit, run separately in CI) - Bundling (that's Webpack or Turbopack)
- Custom Babel plugins (Babel is no longer in the chain when SWC is active)
The custom Babel plugin problem: if your project uses a Babel plugin that SWC doesn't have a native equivalent for, you have to keep Babel in the chain — which means losing SWC's speed advantage for those transforms. This is why teams with custom Babel plugins see slower builds than teams that migrated fully.
For styled-components and Emotion, SWC has built-in transforms that are faster than the Babel plugins:
Turbopack — The Bundler Replacement
Turbopack (also written in Rust) is the replacement for Webpack. Its architectural difference: incremental computation with fine-grained caching.
Webpack's model: build the entire module graph, apply transforms, produce bundles. Incremental builds re-analyse changed modules and their transitive dependents.
Turbopack's model: every module and every function on every module is a cacheable unit of computation. When a file changes, only the computation units that depend on that specific file are re-evaluated. The cache is persistent across restarts — a restart after Turbopack has warmed its cache is nearly as fast as a hot reload.
The practical result: a codebase that took 30 seconds for a hot reload with Webpack might take 500ms with Turbopack, because Turbopack doesn't re-evaluate the 9,800 modules that didn't change.
Enable Turbopack for development:
Turbopack is the default for next dev in Next.js 15. For next build (production), Turbopack is still in progress — production builds use Webpack by default until Turbopack production build reaches parity.
Turbopack Configuration
Turbopack configuration lives in next.config.ts under the turbopack key:
The Webpack loader compatibility note: Turbopack cannot use Webpack loaders directly. If your project uses custom Webpack loaders (SVG transforms, MDX loaders, etc.), you need to find or create Turbopack-compatible versions. This is the primary migration blocker for complex projects.
Bundle Analysis
Before optimising, you need to see what's in your bundle. @next/bundle-analyzer produces a visual map of every module in every chunk:
This opens two HTML files in your browser — one for the client bundle, one for the server bundle. Each file appears as a rectangle sized proportionally to its contribution to the total bundle size.
What to look for:
- Large utility libraries used for one function —
moment(200KB gzipped) when you only need date formatting. Replace withdate-fns(tree-shakeable) or a one-liner. - Duplicate modules — the same library appearing multiple times, often because different parts of the tree import different versions.
- The
node_modulessection — if third-party code dominates, look for lighter alternatives. - Your own code appearing unexpectedly large — usually means a large JSON file or SVG is being bundled inline.
The bundlePagesRouterDependencies Migration Problem
Migrating from Pages Router to App Router often surfaces a bundle size regression: code that was previously Server-only in Pages Router (run only in Node.js) might accidentally be included in the client bundle in the App Router if import boundaries aren't explicit.
The server-only package prevents this at the module level:
If a Client Component accidentally imports from lib/db.ts, the build fails with a clear error. Without server-only, Prisma would end up in the client bundle — you'd see it in the bundle analysis as a mysterious 500KB addition.
Build Performance Profiling
When builds are slow, the first step is measuring where the time goes:
This produces a .next/profile-events.json file you can load in Chrome DevTools' Performance tab or analyse with speedscope. The profile shows which transforms and route compilations take the most time.
Common culprits:
- Type checking during build —
tscrunning as part ofnext build. Move type checking to a separate CI step:npm run typecheckbeforenpm run build. - Large
generateStaticParamsoutputs — generating 100k static pages at build time. Use ISR or PPR with on-demand generation instead. - Slow MDX/content transforms — transforming thousands of markdown files. Cache the transform output.
- Missing barrel file optimisation — importing from index.ts files that re-export hundreds of modules causes the bundler to analyse all of them, even if only one is needed.
optimizePackageImports tells Next.js to import directly from submodules rather than through the barrel index, eliminating the "analyse 500 modules to find the 3 you need" problem.
TypeScript Build Integration
The next build command includes TypeScript checking by default. In large codebases, this can add 60+ seconds to CI build time.
Options:
- Keep it (default) — safest, catches type errors before deployment
- Ignore during build, check in CI separately:
The correct CI pipeline when using ignoreBuildErrors:
This runs type checking in parallel with other checks, potentially cutting CI time significantly.
Where We Go From Here
A-12 focuses on the user experience side of performance: Core Web Vitals engineering — LCP, INP, CLS, and the specific Next.js patterns that affect each metric. After A-11's understanding of how bundles are built, A-12 explains how those bundles affect the metrics that Google measures and users feel.
Why did Next.js transition from Babel to SWC for its JavaScript and TypeScript transformation layer?
How does Turbopack's architectural model differ from Webpack's to achieve significantly faster incremental builds (hot reloads)?
What issue does the server-only package specifically prevent when migrating a large codebase to the Next.js App Router?
Test your knowledge with more question sets
Sign in to access a wider variety of questions and get notified when new practice sets are added to this module.
Sign in & RegisterDiscussion
0Join the discussion