Next.js 16 Turbopack Production Build — Deciding When to Adopt Between Build Speed Gains and Bundle Size Risks
Now that Turbopack has become the default bundler, your team has probably started talking about it. "Is it safe to just use it?" I made the mistake of skimming the release notes and applying it right away, only to discover later — in a cold sweat — that bundle sizes had quietly grown. The App Router's shared client chunks had ballooned unnoticed, and while I was satisfied with the improved CI build times, Core Web Vitals scores — FCP (First Contentful Paint, the time until a user sees the first content on the page) and LCP (Largest Contentful Paint, the time until the largest element renders) — came back to bite me.
This post is written to share a framework for deciding when to adopt Turbopack, based on the number of custom Webpack configurations and bundle analysis metrics, for frontend developers running Next.js apps in production. Rather than abstract claims of "it's faster," we'll look at concrete numbers: under what conditions is it faster, and under what conditions does bundle size grow.
Core Concepts
Turbo Engine: The Principle of "Only Recompute What's Needed"
Reducing Webpack and Turbopack to simply "slow vs. fast" misses an important distinction. Their design philosophies are fundamentally different.
Webpack has supported filesystem caching (the cache option) since v5, storing previous build results on disk for reuse. But this cache operates at the file level. When a single file changes, the module graph connected to that file must be reprocessed.
Turbopack's Turbo Engine goes one level deeper. It stores the result of every function performed during the build — AST parsing, code transformation, chunking, etc. — in a per-function cache unit called a value cell. When a file changes, only the functions that depend on that file are marked "dirty"; everything else reuses already-computed values. If Webpack's file-level cache is "do we rebake the entire file?", Turbopack's approach is "which step in the pipeline do we need to redo from?"
The computation graph looks like this:
Bundle output (root)
→ Chunking result
→ Partially transformed modules
→ AST transformation result
→ Source files (leaves)
When you modify Button.tsx, only the AST parsing → transformation → affected chunk are recomputed, while thousands of other modules reuse their cached value cells.
What is incremental compilation? Instead of rebuilding everything from scratch, only the changed parts and their dependents are selectively recomputed. Webpack supports file-level caching, but Turbopack's differentiator is pushing this granularity down to the function level.
When Did Production Build Support Arrive?
While next dev support has been available for quite some time, the production build (next build) stabilization is comparatively recent.
| Version | Date | Key Change |
|---|---|---|
| Next.js 15.5 | September 2025 | next build Turbopack beta support begins |
| Next.js 16 | October 2025 | Turbopack is the default bundler for both dev and production |
| Next.js 16.1 | December 2025 | Filesystem cache stabilized, bundle analyzer officially integrated |
| Next.js 16.2 | March 2026 | next dev startup speed improved ~400%, Build Adapters API stabilized |
Starting with Next.js 16, the webpack key in next.config.ts is deprecated. To revert to Webpack, you must explicitly pass a CLI flag.
Let's look at how this incremental compilation principle translates into real numbers in a CI environment, with code examples.
Practical Application
Reducing CI Build Times with Filesystem Cache Configuration
The filesystem cache (turbopackFileSystemCacheForBuild) is currently opt-in. It works by saving intermediate compilation results in the .next folder and reusing the unchanged parts in subsequent builds.
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
experimental: {
turbopackFileSystemCacheForBuild: true,
},
}
export default nextConfigLooking at Vercel's official measurements, the impact is substantial:
| Project | Cold Build | With Cache | Improvement |
|---|---|---|---|
| react.dev | 3.7s | 380ms | ~10× |
| nextjs.org | 3.5s | 700ms | ~5× |
| Large internal app | 15s | 1.1s | ~14× |
This effect grows exponentially with project size and is most pronounced in CI environments with frequent repeated builds. For a monorepo with thousands of components, this single option can cut CI builds from several minutes down to tens of seconds.
There is one important caveat: if you don't restore the .next folder cache in CI, this setting has no effect. With GitHub Actions, you need to add a cache step like the following:
# .github/workflows/build.yml
- name: Cache .next folder
uses: actions/cache@v4
with:
path: .next/cache
key: ${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('**/*.ts', '**/*.tsx') }}
restore-keys: |
${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}-Preparing a Webpack Rollback
For projects with many custom Webpack configurations, it's worth keeping a temporary rollback in place while you plan your migration.
# Roll back to Webpack via CLI
next build --webpack{
"scripts": {
"build": "next build",
"build:webpack": "next build --webpack"
}
}Running both scripts in parallel makes it easy to measure bundle size and build time side by side. This is a useful pattern during the migration preparation phase.
Bundle Size Verification — Comparing Numbers Before and After Adoption
It's risky to check only build speed and move on. It's strongly recommended to verify chunk sizes using @next/bundle-analyzer.
Since Next.js 16.1, the bundle analyzer is officially integrated and can be enabled without a separate package. The code below uses the legacy @next/bundle-analyzer wrapper pattern. If you're on 16.1 or later, you can achieve the same result with just the bundleAnalyzer: { enabled: true } key in next.config.ts, without this wrapper.
// next.config.ts (wrapper approach — requires @next/bundle-analyzer package)
import bundleAnalyzer from '@next/bundle-analyzer'
import type { NextConfig } from 'next'
const withBundleAnalyzer = bundleAnalyzer({
enabled: process.env.ANALYZE === 'true',
})
const nextConfig: NextConfig = {
experimental: {
turbopackFileSystemCacheForBuild: true,
},
}
export default withBundleAnalyzer(nextConfig)ANALYZE=true next buildWhy do you need to check bundle size separately? According to CatchMetrics' real-world data, on Next.js 15.5, App Router's shared client chunks increased by approximately 211 kB, with the median First-load JS growing by +279 kB across 151 routes. Because build speed improvements can come at the cost of initial load performance, verification is essential.
Pros and Cons Analysis
Advantages
The filesystem cache deserves special attention. It's the key feature that brings the fast incremental compilation previously enjoyed only in the dev server to production builds as well, and its impact grows exponentially with project size.
| Item | Details |
|---|---|
| Incremental compilation | Only changed modules and dependent functions are recomputed — effect maximizes as codebase grows |
| Parallel processing | Leverages Rust's multi-core support to distribute CPU-intensive work across cores |
| Filesystem cache | Cache persists across process restarts — especially effective in CI environments with repeated builds |
| SWC integration | TypeScript and JSX transpilation handled at the Rust level — no separate tooling required |
| Cold build speed | ~19% faster than Webpack (measured by CatchMetrics on Next.js 15.5.2, ~15,000 LoC app); up to 85% reduction reported for codebases above 50,000 LoC |
Disadvantages and Caveats
Potential bundle size increases are currently the most significant concern. Combined with tree shaking limitations, this can unintentionally include dead code in the bundle. The causes differ for CommonJS and barrel files, so it's worth understanding each separately.
| Item | Details | Mitigation |
|---|---|---|
| No Webpack plugin support | Webpack ecosystem plugins — CSS module plugins, SVG processing plugins, etc. — are incompatible | Inventory dependent plugins and check for equivalent Turbopack options |
| Tree shaking limitation (CommonJS) | CommonJS's dynamic nature makes static analysis impossible, preventing ESM-based tree shaking entirely | Check whether the library has an ESM version and switch accordingly |
| Tree shaking limitation (barrel files) | Barrel files re-exporting many modules from index.ts result in inefficient tree shaking |
Minimize barrel files; switch to direct path imports |
| Potential bundle size increase | App Router shared chunk sizes may grow | Compare before and after migration with @next/bundle-analyzer — mandatory |
| Partial loader compatibility | Loaders based on resourceQuery, image/stylesheet transform loaders are unsupported — only JS-returning loaders are supported |
Audit special loader dependencies in advance |
| Coupled to Next.js | Currently cannot be used standalone without Next.js | If you need a standalone bundler, consider Vite |
What is a barrel file? It's the pattern of re-exporting multiple modules from a single
index.ts(export { Button } from './Button'). While convenient, it makes it harder for the bundler to remove unused code. CommonJS is impossible to statically analyze due to its dynamic structure, whereas barrel files — even in ESM — create complex dependency relationships that reduce tree shaking efficiency.
The Most Common Mistakes in Practice
-
Measuring only build speed and skipping bundle size verification. Being satisfied with faster CI builds can mask a situation where actual users experience slower initial load times. It's worth comparing FCP and LCP metrics before and after the migration.
-
Upgrading without first inventorying custom Webpack configurations. Some teams have pushed upgrades without realizing that SVG loaders and custom font handling were buried inside the
webpackcallback innext.config.ts, only to discover on staging that icons weren't rendering at all on certain pages. Special loaders can be silently ignored by Turbopack, producing incorrect output without throwing any errors. -
Enabling the filesystem cache without setting up CI caching. Some teams enabled
turbopackFileSystemCacheForBuildand were puzzled when build times didn't decrease at all — the cause was that the.nextfolder was being wiped on every GitHub Actions run. The setting only delivers results when applied together with theactions/cachestep described earlier.
Conclusion
Turbopack production builds aren't a question of "when to adopt" but "what to verify for our stack." Teams with minimal custom Webpack configurations and a CI build bottleneck in a large codebase can try it right now. On the other hand, teams with heavy dependencies on special loaders or Webpack plugins are better off going through a migration preparation phase first.
Here are 3 steps you can take right now:
- Extract the list of
webpackcallbacks and special loaders fromnext.config.ts, and check the Turbopack API reference to see if equivalent options exist. - Run
ANALYZE=true next buildin your staging environment and compare bundle sizes against the Webpack output side by side. - If there are no issues, apply
turbopackFileSystemCacheForBuild: truetogether with the GitHub Actions cache configuration.
While this post covered the phenomenon of bundle size increases, we haven't yet addressed when tree shaking actually fails and how to recover from it.
References
- Inside Turbopack: Building Faster by Building Less | Next.js Official Blog
- Next.js 16 Release Notes | Next.js
- Next.js 16.1 Release Notes | Next.js
- Next.js 16.2 Turbopack Analysis | Next.js
- Turbopack API Reference | Next.js Official Docs
- turbopackFileSystemCache Configuration Option | Next.js Official Docs
- Webpack vs Turbopack Performance Comparison | CatchMetrics
- Turbopack is Finally Stable: 5 Real-World Benchmarks vs. Webpack | Medium
- Turbopack in 2026: The Complete Guide | DEV Community
- Turbopack Next.js 16 Migration: Real Tradeoffs, Not Marketing | ByteIota