Privacy Policy© 2026 DEV BAK - TECH BLOG. All rights reserved.
DEV BAK - TECH BLOG
frontend

Speed Up `next build` 10× with Next.js Turbopack Build Cache — Pitfalls of Experimental Flags and CI Integration Strategies

When next build in CI starts exceeding three minutes, the stress quietly accumulates. The time you spend waiting in front of the deployment pipeline every time you open a PR — enough to finish an entire cup of coffee. I ran into the same problem on an e-commerce project where webpack builds were crossing 180 seconds, and I spent a good while thinking "how can I cut this down?" The waiting time was frustrating every time I checked the build locally before a staging deployment, and the same was true in CI. Turbopack's filesystem cache is a feature designed exactly for that problem.

By the end of this article, you'll be able to apply the configuration that brings a 3-minute CI build under 30 seconds right away. The core idea is simple — recompile only the files that changed, and pull everything else straight from disk. However, the build cache is still in an experimental stage, so there are pitfalls. The important caveats are covered alongside the speed gains.


Core Concepts

What Is Incremental Building?

Traditional webpack builds recalculate the entire dependency graph (the structure that tracks import relationships between all files) from scratch every time. Changing a single file means re-bundling the whole app. Turbopack's FileSystem Cache flips that model.

Build artifacts (the dependency graph, intermediate ASTs (the structure that parses source code into a tree), transformed modules, etc.) are persisted in the .next directory, and the next build only recomputes the parts affected by changed files. This is incremental build caching.

It is currently split across two flags:

Flag Target Status
turbopackFileSystemCacheForDev next dev Stable and enabled by default as of Next.js 16.1 release notes
turbopackFileSystemCacheForBuild next build Currently an opt-in experimental flag

Enabling it is straightforward:

typescript
// next.config.ts
const nextConfig = {
  experimental: {
    turbopackFileSystemCacheForBuild: true,
  },
};
export default nextConfig;

TurboEngine — How Cell-Level Caching Works

Digging a little deeper reveals an interesting structure. Inside Turbopack runs TurboEngine, a Rust-based incremental memoization framework. When I first encountered this structure, I thought "isn't it just file hash comparison?" — but in reality it's far more sophisticated.

TurboEngine manages function call results as a "value cell" graph. When a source file changes, only the cells affected by it are marked dirty, and if a cell's content is identical to before, propagation up the graph is short-circuited early.

For example, suppose you modify a single file utils/format.ts — TurboEngine traces only the chain of modules that reference this file and recomputes it. If the output of that chain is identical to before, modules higher up in the graph skip the rebuild. Because tracking happens at the function result cell level rather than the file level, cache invalidation is far more granular.

Being Rust-based also has a direct impact on speed. Because Rust manages memory without GC (garbage collection), the overhead when processing large dependency graphs is significantly reduced compared to JavaScript-based tools.

Differences from webpack 5 Persistent Cache

If you're coming from webpack, the cache: { type: 'filesystem' } option won't be unfamiliar. The concept is similar, but there are differences:

Comparison webpack 5 Persistent Cache Turbopack FileSystem Cache
Tracking unit File and chunk level (output unit bundling multiple modules) Function result cell level
Implementation language JavaScript Rust
Invalidation accuracy Conservative (may trigger excessive rebuilds) Fine-grained dependency tracking
Cache invalidation control Developer manually configures buildDependencies, etc. Automatic tracking, no manual configuration needed

Practical Application

Example 1: Optimizing Local Iterative Builds

This is the scenario where you'll feel the biggest difference. If you frequently verify builds locally before staging deployments, just add a single flag to next.config.ts.

typescript
// next.config.ts
import type { NextConfig } from 'next'
 
const nextConfig: NextConfig = {
  experimental: {
    turbopackFileSystemCacheForBuild: true,
  },
}
 
export default nextConfig

After enabling it, run the build twice and you'll feel the difference immediately:

bash
# First build (cold) — no cache
$ pnpm build
# ✓ Compiled successfully in 15.2s
 
# Second build (warm) after modifying some files — using cache
$ pnpm build
# ✓ Compiled successfully in 1.1s

Below are official benchmarks measured by Vercel through over a year of dogfooding on their own apps. The improvement ratio tends to increase with larger apps:

App Cold Build Cached Build Improvement
react.dev 3.7s 380ms ~10×
nextjs.org 3.5s 700ms ~5×
Large internal app 15s 1.1s ~14×

Numbers will vary depending on project size. For medium-sized apps, ~5× is a realistic expectation.

Example 2: GitHub Actions CI/CD Integration

Cache that only lives locally cuts the benefit in half. Caching the .next directory in CI as well can speed up the entire pipeline.

yaml
# .github/workflows/build.yml
name: Build
 
on:
  push:
    branches: [main, develop]
  pull_request:
 
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
 
      - uses: pnpm/action-setup@v4
        with:
          version: 9  # adjust to match your project version
 
      - uses: actions/setup-node@v4
        with:
          node-version: 22  # adjust to match your project version
          cache: 'pnpm'
 
      - name: Cache .next directory
        uses: actions/cache@v4
        with:
          path: .next
          key: ${{ runner.os }}-next-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('src/**/*.ts', 'src/**/*.tsx') }}
          restore-keys: |
            ${{ runner.os }}-next-${{ hashFiles('**/pnpm-lock.yaml') }}-
            ${{ runner.os }}-next-
 
      - name: Install dependencies
        run: pnpm install --frozen-lockfile
 
      - name: Build
        run: pnpm build

There is one key point in the cache key design. When pnpm-lock.yaml changes (dependency update), the cache is discarded; when TypeScript files inside src/ change, a partial hit is attempted via restore-keys. A partial hit is far better than a complete cache miss. Setting the scope too broadly like **/*.ts can include type files inside node_modules in the hash, causing unnecessary cache key churn — narrowing it to src/**/*.ts is more practical.

Managing the flag in next.config.ts is the standard approach. Controlling it via CI environment variables is not separately documented in the official docs, so operating from the configuration file is recommended.

Example 3: Resetting the Cache When Corruption Occurs

In practice, you may encounter situations where "the build succeeds but runtime behavior is strange," or "the build itself fails with an inexplicable error." This is when you should suspect cache corruption.

bash
# Full cache reset — recommended to delete in this order
rm -rf .next
rm -rf node_modules/.cache
rm -rf .turbo
 
# Clean build
pnpm build

It may feel wasteful to discard the cache, but this is the most reliable approach in this situation. The subsequent build will be a cold build, but if the issue was caused by cache corruption, it will be resolved immediately.


Pros and Cons Analysis

Honestly, looking at the list of advantages alone makes you want to push it to production right away — but having actually gone through this myself, the downsides stung harder. Here's a candid breakdown of both.

Advantages

Item Details
Repeated build speed Warm builds: from tens of seconds down to hundreds of milliseconds. A tangible improvement
Fine-grained cache invalidation Cell-level tracking instead of file-level prevents excessive rebuilds
Automatic tracking Developers don't need to manually manage cache keys or dependencies
next dev restart improvement Resumes immediately from prior compilation state, no warm-up time

Disadvantages and Caveats

Item Details Mitigation
Build cache is experimental Cache invalidation accuracy is still being validated Thorough testing recommended before production deployment
Cache corruption risk Cases of build errors from cache corruption have been reported in the Vercel community Rebuild after deleting .next, node_modules/.cache, and .turbo
CI overhead Upload/download time grows as the .next cache gets larger. Benefits diminish if the cache hit rate (the ratio of builds that skip recomputation) falls below 80% Fine-tune cache key strategy
Phantom module reference bug Cases where the cache still references deleted files occur intermittently (GitHub Discussion #89669) Resolvable with a full cache reset
Environment variable invalidation Verify that the cache is properly invalidated when build environment variables change Recommend a clean build for the first deployment after changing environment variables

The Most Common Mistakes in Practice

  1. Comparing a webpack cold build to a Turbopack warm build. You need to compare under identical conditions (both cold or both warm) to get an accurate baseline difference. Comparing the first build without cache between the two will show you the real difference.
  2. Setting CI cache keys too broadly. It's recommended to include the lock file hash in the key so the cache is discarded when dependencies change. Otherwise, corrupted cache can keep getting restored.
  3. Mistaking cache corruption symptoms for code bugs. When a build succeeds but runtime behavior is strange, or changes you clearly made don't seem to be reflected, try clearing the cache first.

Closing Thoughts

Turbopack FileSystem Cache is a practical technology that improves repeated build speeds by several to tens of times, but since the build cache is still in an experimental stage, the smart approach is to adopt it while sufficiently validating cache invalidation accuracy.

That 3-minute build wait you faced every time you opened a PR — it can be different now. Here are 3 steps you can start with right away:

  1. Add the flag and measure the difference — Add experimental: { turbopackFileSystemCacheForBuild: true } to next.config.ts, run pnpm build twice, and measure the cold/warm build time difference yourself.
  2. Connect CI caching — Cache the .next directory with actions/cache@v4, and include the pnpm-lock.yaml hash in the cache key so it automatically refreshes when dependencies change.
  3. Verify with a clean build before deployment — Making it a habit to compare rm -rf .next && pnpm build output against a cached build to confirm the outputs match can proactively prevent unexpected issues from cache corruption.

References

  • next.config.js: turbopackFileSystemCache | Next.js Official Docs (cited in article)
  • Next.js 16 Official Release Notes
  • Next.js 16.1 Official Release Notes (cited in article)
  • Inside Turbopack: Building Faster by Building Less (cited in article)
  • Turbopack File System Caching Feedback · vercel/next.js Discussion #87283
  • Turbopack Phantom Module References Causing Panic · Discussion #89669 (cited in article)
  • Turbopack Persistent Caching — Tobias Koppers (JSNation)
  • Next.js 16.2 Turbopack: The Anatomy of a 400% Improvement — DEV Community
  • Turbopack corrupted build cache — Vercel Community (cited in article)
  • next.config.js: turbopackPersistentCaching | Next.js 15 Docs
#NextJS#Turbopack#증분빌드#빌드캐시#TypeScript#GitHubActions#CI-CD#webpack#Rust#pnpm
Share

Table of Contents

Core ConceptsWhat Is Incremental Building?TurboEngine — How Cell-Level Caching WorksDifferences from webpack 5 Persistent CachePractical ApplicationExample 1: Optimizing Local Iterative BuildsExample 2: GitHub Actions CI/CD IntegrationExample 3: Resetting the Cache When Corruption OccursPros and Cons AnalysisAdvantagesDisadvantages and CaveatsThe Most Common Mistakes in PracticeClosing ThoughtsReferences

Recommended Posts

How to Reduce TTFB from 350ms to 60ms with Next.js RSC + Streaming
frontend

How to Reduce TTFB from 350ms to 60ms with Next.js RSC + Streaming

While reviewing the Core Web Vitals of a production service, I once discovered pages where TTFB was well over 400ms. Each DB query was individually fast, so I w...

June 7, 202619 min read
HTMX 4.0 Server Rendering Patterns: Architecture Choices for Building Interactive Web Apps Without Client State
frontend

HTMX 4.0 Server Rendering Patterns: Architecture Choices for Building Interactive Web Apps Without Client State

When I first learned React, I honestly buried this question in the back of my mind: "Does clicking a single button really require this much code?" State managem...

June 7, 202622 min read
View Transitions API — Production Page Transition Animations Without Libraries, After Achieving Baseline 2025
frontend

View Transitions API — Production Page Transition Animations Without Libraries, After Achieving Baseline 2025

Honestly, my first reaction when I saw this API was "this actually works?" The idea of implementing app-like page transitions with just a few lines of CSS and a...

June 7, 202620 min read
Zustand persist migrate: 4 Ways to Safely Narrow `persistedState unknown` Type with TypeScript
frontend

Zustand persist migrate: 4 Ways to Safely Narrow `persistedState unknown` Type with TypeScript

When persisting state to localStorage with Zustand, there inevitably comes a moment when you need to change the schema. Renaming fields, adding new ones, flatte...

May 30, 202619 min read
TanStack Query + Zustand: Patterns and Anti-Patterns for Separating Server State and Client State
frontend

TanStack Query + Zustand: Patterns and Anti-Patterns for Separating Server State and Client State

Looking back at the days of using Redux, I remember API responses and modal open/close states all jumbled together in a single store. There were plenty of days ...

May 24, 202619 min read
Adding Pure CSS Entry & Exit Animations to Popovers and Dialogs with `@starting-style`
frontend

Adding Pure CSS Entry & Exit Animations to Popovers and Dialogs with `@starting-style`

That jarring pop whenever a popover opens — you've probably found it annoying at least once. And when it closes, it just vanishes. Open up the JS to smooth thin...

May 22, 202617 min read