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 build and 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.
ts// next.config.ts — disable SWC if you have incompatible Babel transforms const config: NextConfig = { swcMinify: true, // use SWC for minification (default true in Next.js 13+) // To keep Babel (opt-out of SWC): // Create .babelrc or babel.config.js — Next.js automatically falls back to Babel // when it detects a Babel config file };
For styled-components and Emotion, SWC has built-in transforms that are faster than the Babel plugins:
ts// next.config.ts const config: NextConfig = { compiler: { styledComponents: true, emotion: true, // removeConsole: { exclude: ['error'] }, // strip console.log in production }, };
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:
bashnext dev --turbopack # or in package.json: # "dev": "next dev --turbopack"
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:
ts// next.config.ts const config: NextConfig = { turbopack: { // Custom module resolution aliases resolveAlias: { '@': './src', 'lodash': 'lodash-es', // use ESM version of lodash }, // Custom file extensions to resolve resolveExtensions: ['.tsx', '.ts', '.jsx', '.js', '.json'], // Custom loaders (replaces Webpack loaders for Turbopack) rules: { '*.svg': { loaders: ['@svgr/webpack'], as: '*.js', }, }, }, };
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:
bashnpm install @next/bundle-analyzer
ts// next.config.ts import bundleAnalyzer from '@next/bundle-analyzer'; const withBundleAnalyzer = bundleAnalyzer({ enabled: process.env.ANALYZE === 'true', }); const config: NextConfig = { // your config }; export default withBundleAnalyzer(config);
bashANALYZE=true npm run build
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:
ts// lib/db.ts import 'server-only'; // throws if this module is imported by client code import { PrismaClient } from '@prisma/client'; export const db = new PrismaClient();
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:
bash# Profile the build (Next.js 15+) NEXT_PROFILE=true npm run build
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.
ts// next.config.ts — optimise barrel imports (also called package imports) const config: NextConfig = { experimental: { optimizePackageImports: ['@radix-ui/react-icons', 'lucide-react', 'date-fns'], }, };
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:
ts// next.config.ts const config: NextConfig = { typescript: { // Allows production builds to succeed even if there are TypeScript errors // (use this only if you have a separate typecheck step in CI) ignoreBuildErrors: true, }, };
The correct CI pipeline when using ignoreBuildErrors:
yamlsteps: - run: npm run typecheck # npx tsc --noEmit - run: npm run lint - run: npm run test - run: npm run build # fast build, no type check
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.