
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
tscor your editor. - It's not a separate tool you install. There's no
turbopackpackage to add. It ships inside thenextpackage. - 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:
| Feature | Notes |
|---|---|
| TypeScript and JSX | Compiled by SWC. No type checking. |
| Fast Refresh | Works for JS, TS, and CSS changes. |
| Server Components | Correct client/server bundling for the App Router. |
| Global CSS and CSS Modules | Processed by Lightning CSS, including nesting. |
| PostCSS | Picks up postcss.config.* automatically, so Tailwind works. |
| Sass/SCSS | Supported out of the box. |
| Path aliases | Reads paths and baseUrl from tsconfig.json. |
| Babel | Used automatically if a Babel config file exists (since v16). |
| Images, fonts, JSON | Static 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:
| Option | What it does |
|---|---|
rules | Run webpack loaders on matching files. |
resolveAlias | Redirect one import specifier to another module. |
resolveExtensions | Change the list of file extensions tried during resolution. |
root | Set the filesystem root Turbopack is allowed to resolve from. |
debugIds | Emit 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:
- Move the config to Turbopack. Translate each piece of the
webpack()function intoturbopackoptions. Most configs are small and map cleanly. - Keep webpack for now. Add
--webpackto the affected scripts. - Ignore the webpack config. Run
next build --turbopackto build with Turbopack and skip thewebpack()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:
| webpack | Turbopack |
|---|---|
module.rules with a loader | turbopack.rules |
resolve.alias | turbopack.resolveAlias |
resolve.fallback: { fs: false } | turbopack.resolveAlias with a browser condition |
resolve.extensions | turbopack.resolveExtensions |
asset/resource, raw-loader | rule type: "asset" or type: "raw" |
| Webpack plugins | Not 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 addresolveAlias: { "~*": "*" }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-heightorletter-spacingslightly. - Unsupported features. Custom Sass functions via
sassOptions.functions, Yarn PnP, standalone:local/:globalpseudo-classes in CSS Modules, and@valuein 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.


