ZYVOPMulti-Platform Sync
SeriesAI NewsWhy ZyVOPJoin Discord
LoginGet Started
ZYVOP
The Developer Publishing Hub
PrivacyTermsGuidelinesDMCACommunity
© 2026 ZyVOP
HomeTurbopack's Persistent Build Cache Only Helps If Your CI Keeps It

Turbopack's Persistent Build Cache Only Helps If Your CI Keeps It

Danny Holloran
Danny Holloran
Senior Developer
September 21, 2026
4 min read
Turbopack's Persistent Build Cache Only Helps If Your CI Keeps It
#bundlers#Next.js#tooling#performance

You upgrade to Next.js 16.3, read that Turbopack's persistent file system cache is now on by default for next build, see a chart claiming up to 5.5x faster builds, push to CI, and watch your pipeline take exactly as long as it did yesterday. Nothing is broken. The feature is doing precisely what it says. The problem is that "persistent" means persisted to a directory, and your build container throws that directory away the moment it exits.

This is the least glamorous kind of performance work: the speedup is real, it's free, and collecting it is an infrastructure chore rather than a code change.

The cache is a directory, not a feature flag

Turbopack has been persisting its incremental compilation cache to disk for next dev since 16.1. In 16.3 the same mechanism graduated for next build, and both are enabled by default:

// next.config.ts
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  experimental: {
    turbopackFileSystemCacheForDev: true, // default
    turbopackFileSystemCacheForBuild: true, // default
  },
};

export default nextConfig;

Dev writes to .next/dev/cache/turbopack, builds write to .next/cache/turbopack. On the second build Turbopack reads those entries off disk before compiling anything new, so you only pay for what changed. Vercel's published numbers span a wide range depending on how much of the route graph the change touches: nextjs.org went from 21s cold to 9.2s warm, vercel.com/home from 66s to 46s, and vercel.com/geist from 30s to 5.5s.

Every one of those numbers assumes .next/cache survives between runs. Restoring it in GitHub Actions is a handful of lines:

- uses: actions/cache@v4
  with:
    path: |
      ~/.npm
      ${{ github.workspace }}/.next/cache
    key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**/*.[jt]s', '**/*.[jt]sx') }}
    restore-keys: |
      ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-

The restore-keys fallback matters more than the exact key. If your lockfile is unchanged but source files moved, you still want the previous cache as a starting point instead of a cold compile.

Docker is the case that quietly eats this. A containerized build starts from a clean layer, so unless you explicitly mount a cache or use RUN --mount=type=cache, .next/cache is empty on every single build and Turbopack spends time writing a cache that nothing will ever read. If that describes your setup and you don't plan to fix it, turn the thing off rather than paying for it:

experimental: {
  turbopackFileSystemCacheForBuild: false,
}

That's the actual decision in front of you: persist the directory, or opt out. Leaving it on with nothing to restore is the one configuration that costs you something and returns nothing.

The dev-memory win needs no setup at all

The other half of the release is the part you'll notice without touching any config. Because cached results are now safely on disk, Turbopack can evict them from memory instead of holding every visited route forever. After compiling 50 routes, Vercel measured its own dashboard app dropping from 21.5 GB to 2 GB, and nextjs.org from 4,600 MB to 840 MB.

That's roughly a 90% and 82% reduction, and it lands in a moment when your dev machine is more crowded than it used to be. A coding agent, a TypeScript server, a linter, and a test watcher are all competing for the same RAM as your bundler. Eviction requires the dev file system cache to be enabled, and both are on by default. The escape hatch exists if you're debugging cache behavior itself:

experimental: {
  turbopackMemoryEviction: false, // default is 'auto'
}

Your mileage genuinely varies here. The reduction depends on the size of your route graph, how much of it you touched, and how long the session ran. A three-route side project has nothing to evict.

Two smaller things worth a look

The React Compiler has been stable in Next.js since 16.0, but only as a Babel transform, which meant large apps waited on JS execution during builds. The React team shipped a native Rust port, and Turbopack now wires it up behind an experimental flag with early tests on large apps showing 20-50% compilation wins:

const nextConfig = {
  reactCompiler: true,
  experimental: {
    turbopackRustReactCompiler: true,
  },
};

Turbopack also picked up Vite's import.meta.glob, which is a small quality-of-life upgrade for anyone hand-maintaining a list of content files:

const posts = import.meta.glob("./posts/*.mdx");

for (const path in posts) {
  const post = await posts[path]();
}

Pass eager: true to import each match immediately. It's Turbopack-only, so it won't work if you're still building with --webpack.

Of the four, the build cache is the one with a real chance of being invisible to you. Before you file the 16.3 upgrade as done, go look at whether .next/cache actually survives a CI run. If it doesn't, you've been paying for a cache you never read.

Sources: the Turbopack 16.3 release post, the turbopackFileSystemCache reference, and the CI build caching guide.

Comments (0)

Join the discussion by logging into your account.

Danny Holloran
Danny Holloran

Senior Developer

Senior Frontend & Fullstack Developer with 14+ years building performant, scalable web applications. Passionate about architecture, mentorship, and finding the right tool for the job.

Subscribe to Danny Holloran's Newsletter

Direct email dispatches when new stories are published. Zero algorithms.

Danny Holloran
Like
Love
Clap
Fire
Party
Wow

More from Danny Holloran

View profile

CSS reading-flow: Fix the Tab Order Your Layout Broke

Grid and flexbox let you rearrange a layout without touching the DOM, which quietly breaks keyboard navigation. The reading-flow and reading-order properties finally let CSS tell the browser which order actually counts.

3 minSep 19

WebNN: The Only Web API That Can Reach Your NPU

Almost every laptop shipped in the last two years has a neural processing unit sitting idle. WebNN is the only web standard that can actually talk to it, and it just hit an updated Candidate Recommendation.

4 minSep 15

GraphQL @oneOf: Exactly One Input, Enforced by the Schema

OneOf Input Objects landed in the September 2025 GraphQL spec, which means the exactly-one-of-these-arguments rule you've been enforcing in resolver code is now something the type system can do for you.

3 minSep 14

node --test: The Test Runner You Already Have Installed

Node's built-in test runner has been stable since v20 and now handles mocking, coverage, watch mode, and TypeScript files. Here's what it does well and where it still falls short.

3 minSep 12

Svelte Snippets: Reuse Markup Without a New Component

Snippets let you define reusable chunks of markup inline and render them like functions. They replace slots, kill the let: directive, and mean you stop extracting a component every time you repeat six lines.

3 minSep 8