Type something to search...
Turbopack in Next.js: What It Is and How It Speeds Up Development

Turbopack in Next.js: What It Is and How It Speeds Up Development

If you started a Next.js project recently, you're already using Turbopack. Since Next.js 16 it's the default bundler for both next dev and next build, and you don't have to pass a flag or change a config file to get it. Most people only notice it indirectly: the dev server starts faster, routes compile faster the first time you visit them, and edits show up in the browser almost before you've switched windows.

That "it just works" quality is great until something doesn't. Maybe an old webpack() function in your config now breaks the build, a Sass import with a ~ prefix can't be resolved, or you want to know whether the speedup is real on your codebase. This post explains what Turbopack actually is, the specific design choices that make it fast, how to configure it, and what to watch for when moving an existing webpack setup over.

What Turbopack Is

Turbopack is an incremental bundler for JavaScript and TypeScript, written in Rust and built directly into Next.js. It takes the same job webpack used to do (resolving imports, compiling TypeScript and JSX, processing CSS, splitting code into chunks) and does it with a different architecture.

A few things it is not:

  • It's not a type checker. Like webpack with SWC, Turbopack strips types and compiles your code. Type errors still come from tsc or your editor.
  • It's not a separate tool you install. There's no turbopack package to add. It ships inside the next package.
  • It's not Turborepo. The names are similar and both come from Vercel, but Turborepo is a monorepo task runner. Turbopack is a bundler.

Under the hood, Turbopack uses SWC for compiling JavaScript and TypeScript and Lightning CSS for CSS. Both are Rust-based too, so the whole pipeline avoids most of the JavaScript overhead webpack carried.

Why It's Faster

"Written in Rust" is part of the story, but most of the speedup comes from how Turbopack decides what work to do and what work to skip.

Incremental computation

Turbopack breaks the build into many small functions and caches the result of each one. When a file changes, it only re-runs the functions whose inputs actually changed and reuses everything else. Edit one component and Turbopack recompiles that module and whatever directly depends on it, not the whole route.

That caching goes down to the function level, and it's parallelized across your CPU cores. In a large app, this is the difference between a save that takes a couple of seconds and one that feels instant.

Lazy bundling

In development, Turbopack only compiles what the dev server is actually asked for. If your app has 300 routes and you open /dashboard, it compiles /dashboard and its dependencies. The other 299 routes are left alone until you visit them.

This is why the first next dev startup on a big project is so much quicker than it used to be. Nothing is compiled ahead of time unless it's requested.

One graph for client and server

A Next.js app produces several outputs: client bundles for the browser, server bundles for Server Components and Route Handlers, and so on. Webpack handled this with multiple compilers whose output had to be stitched together. Turbopack uses a single unified module graph for every environment, which avoids duplicated work and keeps the server/client boundary consistent.

Bundling in dev, but smartly

Some tools skip bundling during development and serve native ES modules straight to the browser. That's fast for small apps, but large apps end up making thousands of network requests on page load. Turbopack still bundles in development, but incrementally, so you get a manageable number of requests without paying the cost of a full rebuild.

A cache that survives restarts

Turbopack's results are persisted to disk. In Next.js 16.1 the filesystem cache became the default for next dev, and in 16.3 it became the default for next build as well. Restart the dev server and it picks up where it left off instead of compiling everything again.

The dev cache lives in .next/dev/cache/turbopack and the build cache in .next/cache/turbopack. Both are controlled by two flags in next.config.ts:

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

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

export default nextConfig;

You only need to touch these if you want to opt out. The build cache in particular only helps if .next/cache is restored between builds. If your CI or Docker build always starts from a clean directory and you never cache that folder, set turbopackFileSystemCacheForBuild to false so Next.js doesn't waste time writing a cache nobody reads. If you do want warm builds in CI, cache .next/cache with your CI provider's cache step.

Using Turbopack (and Opting Out)

Because it's the default, your scripts don't need anything special:

{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start"
  }
}

If you're upgrading from Next.js 15, you may still have --turbopack or --turbo flags in your scripts. They're no longer necessary and you can remove them.

To use webpack instead, pass --webpack:

{
  "scripts": {
    "dev": "next dev",
    "build": "next build --webpack",
    "start": "next start"
  }
}

This example keeps Turbopack for development and uses webpack for production builds, which is a reasonable halfway house while you migrate a complex webpack config. The recommendation from the Next.js team is to use Turbopack for both once you can.

One more case where you'll need webpack: platforms without native bindings. Turbopack ships native binaries for macOS, Windows, and Linux (glibc and musl) on x64 and ARM64. On something like FreeBSD, Next.js falls back to WebAssembly bindings, which handle SWC compilation but not Turbopack, so you'd run with --webpack there.

What Works Out of the Box

Most of what you use day to day needs no configuration at all:

FeatureNotes
TypeScript and JSXCompiled by SWC. No type checking.
Fast RefreshWorks for JS, TS, and CSS changes.
Server ComponentsCorrect client/server bundling for the App Router.
Global CSS and CSS ModulesProcessed by Lightning CSS, including nesting.
PostCSSPicks up postcss.config.* automatically, so Tailwind works.
Sass/SCSSSupported out of the box.
Path aliasesReads paths and baseUrl from tsconfig.json.
BabelUsed automatically if a Babel config file exists (since v16).
Images, fonts, JSONStatic imports work as you'd expect.

The Babel row is worth calling out. With webpack, adding a .babelrc disabled SWC entirely. Turbopack still uses SWC for Next.js's own transforms and only runs Babel on top for your code when it finds a config file. If you added Babel years ago for a single plugin you no longer need, deleting the config file removes that extra step.

If you're setting up styling, see how to integrate CSS and Sass in Next.js and what's new in Tailwind CSS v4. Both work with Turbopack without changes.

Configuring Turbopack

When you do need to customize something, use the top-level turbopack key in next.config.ts. In Next.js 15 and earlier this lived under experimental.turbo or experimental.turbopack; it's now stable and top-level.

The options you're most likely to reach for:

OptionWhat it does
rulesRun webpack loaders on matching files.
resolveAliasRedirect one import specifier to another module.
resolveExtensionsChange the list of file extensions tried during resolution.
rootSet the filesystem root Turbopack is allowed to resolve from.
debugIdsEmit debug IDs in bundles and source maps.

Running webpack loaders with rules

Turbopack can't run webpack plugins, but it can run many webpack loaders. The classic example is turning SVG imports into React components with SVGR:

npm install -D @svgr/webpack
// next.config.ts
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  turbopack: {
    rules: {
      "*.svg": {
        loaders: ["@svgr/webpack"],
        as: "*.js",
      },
    },
  },
};

export default nextConfig;

The key is a glob matched against the file name. loaders lists the loaders to run in order, and as tells Turbopack how to treat the output: here, the SVG becomes a JavaScript module. Now you can import an icon as a component:

// app/components/Header.tsx
import Logo from "./logo.svg";

export default function Header() {
  return (
    <header className="flex items-center gap-2">
      <Logo width={32} height={32} aria-hidden />
      <span>TideWave</span>
    </header>
  );
}

If you need different handling depending on where code runs, rules can carry conditions. A common one is skipping files in node_modules with the built-in foreign condition, or targeting only browser code with browser:

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

const nextConfig: NextConfig = {
  turbopack: {
    rules: {
      "*.svg": {
        condition: { not: "foreign" },
        loaders: ["@svgr/webpack"],
        as: "*.js",
      },
    },
  },
};

export default nextConfig;

Keep in mind that only loaders returning JavaScript are supported, and loader options must be plain serializable values (strings, numbers, objects, arrays). You can't pass a require()d plugin as an option.

Treating files as raw text or assets

Since Next.js 16.2, a rule can set a module type directly without any loader. For example, to import .txt files as strings:

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

const nextConfig: NextConfig = {
  turbopack: {
    rules: {
      "*.txt": {
        type: "raw",
      },
    },
  },
};

export default nextConfig;

Other useful types are asset (emit the file and return its URL) and bytes. This replaces a lot of small loaders like raw-loader that people used to install just for one file type.

Aliases

resolveAlias is the Turbopack equivalent of webpack's resolve.alias. It's also how you handle the old resolve.fallback trick for Node.js built-ins that sneak into client bundles:

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

const nextConfig: NextConfig = {
  turbopack: {
    resolveAlias: {
      // Load an empty module when browser code imports "fs"
      fs: { browser: "./empty.ts" },
    },
  },
};

export default nextConfig;

empty.ts can just be export {};. Treat this as a last resort though. The real fix is making sure client code never imports a module that needs fs.

Monorepos and linked packages

Turbopack refuses to resolve files outside the project root. It detects the root by looking for a lockfile (package-lock.json, pnpm-lock.yaml, yarn.lock, or bun.lock). In a typical monorepo this just works. If you're linking a package from a sibling folder with npm link, point root at a directory that contains both:

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

const nextConfig: NextConfig = {
  turbopack: {
    root: path.resolve(process.cwd(), ".."),
  },
};

export default nextConfig;

Turbopack-Only Extras

Two Vite-style features are available only when you build with Turbopack.

import.meta.env exposes DEV, PROD, MODE, BASE_URL, and SSR. These are statically analyzed, so code inside an if (import.meta.env.DEV) block is removed entirely from production bundles:

// lib/log.ts
export function debug(...args: unknown[]) {
  if (import.meta.env.DEV) {
    console.log("[debug]", ...args);
  }
}

Custom VITE_* variables aren't supported; keep using process.env.NEXT_PUBLIC_* for your own values.

import.meta.glob() imports many modules at once by pattern. It's handy for things like loading every file in a folder of content or plugins:

// lib/widgets.ts
const modules = import.meta.glob("./widgets/*.tsx", { import: "default" });

export async function loadWidget(name: string) {
  const load = modules[`./widgets/${name}.tsx`];
  if (!load) throw new Error(`Unknown widget: ${name}`);
  return load();
}

By default each entry is a function returning a promise, so modules are loaded lazily. Pass { eager: true } to import them all up front. Both features break if you switch back to --webpack, so use them only if you've committed to Turbopack.

Migrating From a webpack Config

This is where most upgrade pain comes from. If next.config contains a webpack() function and you run next build, Next.js 16 fails the build rather than silently ignoring your config. That's intentional: your webpack customizations won't run under Turbopack, and the build would otherwise produce something different from what you expect.

You have three options:

  1. Move the config to Turbopack. Translate each piece of the webpack() function into turbopack options. Most configs are small and map cleanly.
  2. Keep webpack for now. Add --webpack to the affected scripts.
  3. Ignore the webpack config. Run next build --turbopack to build with Turbopack and skip the webpack() function, if you know it's not needed.

If you don't remember writing a webpack() function, check your plugins. Wrappers like older MDX or bundle analyzer integrations sometimes inject one.

Here's how common webpack customizations translate:

webpackTurbopack
module.rules with a loaderturbopack.rules
resolve.aliasturbopack.resolveAlias
resolve.fallback: { fs: false }turbopack.resolveAlias with a browser condition
resolve.extensionsturbopack.resolveExtensions
asset/resource, raw-loaderrule type: "asset" or type: "raw"
Webpack pluginsNot supported; find an alternative or stay on webpack

Behavior differences to check

A few things behave differently and occasionally cause visual or resolution changes after switching:

  • Sass tilde imports. @import "~bootstrap/..." doesn't work. Drop the ~, or add resolveAlias: { "~*": "*" } if you can't edit the imports.
  • CSS Module ordering. Turbopack orders CSS Modules by JS import order. If your styles relied on an accidental order, you may see different rules winning. Fix it by making the dependency explicit or by not targeting the same properties from two modules.
  • CSS precision. Lightning CSS rounds computed values to 5 decimal places instead of webpack's 10. It rarely matters, but it can nudge line-height or letter-spacing slightly.
  • Unsupported features. Custom Sass functions via sassOptions.functions, Yarn PnP, standalone :local/:global pseudo-classes in CSS Modules, and @value in CSS Modules aren't supported.

Measuring the Difference

Don't take the speedup on faith. A simple before/after comparison on your own project is more convincing than any benchmark:

# Turbopack (default)
rm -rf .next && time npx next build

# webpack, for comparison
rm -rf .next && time npx next build --webpack

For development, open your heaviest route, note the compile time Next.js prints in the terminal, then edit a component and watch how long the update takes. Run it once with next dev and once with next dev --webpack. Then restart the Turbopack server without deleting .next to see the filesystem cache at work: the same route should come back far faster the second time.

If something is slow and you want to know why, Turbopack can record a trace:

npm run dev -- --internal-trace

Use the app until you've reproduced the problem, stop the server, and inspect .next-profiles/trace-turbopack.bin:

npx next internal trace .next-profiles/trace-turbopack.bin

Then open https://trace.nextjs.org/ to see which modules took the longest to compile. A single giant barrel file or a heavy icon library imported in full often shows up immediately. For looking at what ends up in your production bundles rather than compile time, next experimental-analyze runs a Turbopack-based bundle analyzer; our post on analyzing and reducing bundle size covers that side.

FAQ

Do I need to install anything to use Turbopack? No. It's part of the next package and is the default from Next.js 16 onward.

Does Turbopack type-check my code? No. Run tsc --noEmit in CI or rely on your editor. next build still runs its own TypeScript check as a separate step.

Can I use webpack plugins with Turbopack? No. Loaders are supported; plugins aren't. If a plugin is essential, keep that build on --webpack until there's an alternative.

Is it safe for production builds? Yes. Turbopack has been the default for next build since Next.js 16, and the build cache is on by default since 16.3.

Conclusion

Turbopack makes Next.js faster mainly by doing less: it compiles only the routes you request, re-runs only the work affected by a change, shares one module graph across client and server, and keeps its results on disk between runs. For a new project there's nothing to configure. For an existing one, the work is mostly translating a webpack() function into turbopack.rules and turbopack.resolveAlias, fixing a few Sass or CSS ordering quirks, and dropping flags you no longer need. Time a build both ways on your own code and you'll see whether it's worth it. In most projects it clearly is.

Tags :
Share :

Related Posts

A Deep Dive into next.config Options Every Developer Should Know

A Deep Dive into next.config Options Every Developer Should Know

next.config.ts is the one file every Next.js project has and almost nobody reads end to end. It starts as an empty object, then slowly collects a r

Continue Reading
Adding JSON-LD Structured Data to Next.js Pages for Rich Search Results

Adding JSON-LD Structured Data to Next.js Pages for Rich Search Results

Search engines are good at reading pages, but they still guess. Is "4.7" a rating or a version number? Is that date when the article was published or

Continue Reading
Adding Page Transitions and Animations to Next.js with Framer Motion

Adding Page Transitions and Animations to Next.js with Framer Motion

Animation is one of the easiest ways to make an app feel polished, and one of the easiest ways to make it feel slow. A subtle fade when a page loads,

Continue Reading