
How to Use Turbopack to Speed Up Next.js Development?
Anyone who has worked on a large Next.js project with webpack knows the routine: start the dev server, wait half a minute for the first compile, click a link to a route you have not visited yet, wait again, save a file, and watch Fast Refresh take a second or two to catch up. Multiply that by a few hundred saves a day and slow tooling becomes one of the biggest drains on a team's productivity.
Turbopack is the answer Next.js ships for that problem. It is an incremental bundler written in Rust and built directly into Next.js, and since Next.js 16 it is the default bundler for both next dev and next build. Most projects get it without changing anything, but projects with a custom webpack() config, Sass tricks, SVG loaders, or monorepo setups need a little work to get the most out of it.
This article covers what Turbopack is and why it is faster, how to use it in new and existing projects, how to migrate common webpack customizations to the turbopack config, how the filesystem cache works, the behavior differences you may hit when switching, how to analyze your bundles, and how to troubleshoot problems.
What Turbopack Is
Turbopack is the successor to webpack inside Next.js. It handles the same jobs: resolving modules, compiling TypeScript and JSX, processing CSS, splitting code into chunks, and powering Fast Refresh. It does them with a different architecture:
- Incremental computation. Turbopack caches work down to the function level and parallelizes across CPU cores. Once something has been computed, it is not recomputed unless its inputs change.
- Lazy bundling. In development, it only compiles what the browser actually requests. A route you never open is never bundled.
- One unified graph. Next.js builds for several environments at once: the browser, the Node.js server, and React Server Components. Turbopack uses a single graph for all of them instead of stitching together separate compilers.
- Persistent cache. Results are stored on disk, so restarting the dev server or running another build starts warm.
For JavaScript and TypeScript transforms, Turbopack uses SWC. For CSS, it uses Lightning CSS. Neither runs type checking, so you still need tsc or your editor for type errors.
| Capability | webpack in Next.js | Turbopack in Next.js 16 |
|---|---|---|
Default for next dev | No (opt in with --webpack) | Yes |
Default for next build | No (opt in with --webpack) | Yes |
| Language | JavaScript | Rust |
| Compiles unvisited routes | Often, depending on config | No, lazy in development |
| Persistent disk cache | Yes | Yes, for dev and build by default |
| webpack loaders | Yes | Many, via turbopack.rules |
| webpack plugins | Yes | No |
Using Turbopack in Next.js 16
New projects
There is nothing to enable. A project created with create-next-app has plain scripts, and both commands use Turbopack:
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
}
}
When the dev server starts, the terminal banner shows Next.js 16.x (Turbopack), which is the quickest way to confirm which bundler is running.
Projects upgraded from Next.js 15
In Next.js 15 you enabled Turbopack with a flag, so many projects still have scripts like these:
{
"scripts": {
"dev": "next dev --turbopack",
"build": "next build --turbopack"
}
}
The flag is now redundant. Remove it to keep the scripts clean. If you still have Turbopack options under experimental.turbo or experimental.turbopack, move them to the top-level turbopack key. A codemod handles the rename:
npx @next/codemod@latest next-experimental-turbo-to-turbopack .
To bring an older project up to date first, see how to update to the latest version of Next.js.
Opting out with --webpack
If something in your project does not work with Turbopack yet, you can keep webpack for one or both commands:
{
"scripts": {
"dev": "next dev",
"build": "next build --webpack",
"start": "next start"
}
}
A common transition path is to use Turbopack in development right away and keep webpack for production builds until you have verified the output.
Why Your Build Fails With a webpack Config
The first problem many teams hit after upgrading to Next.js 16 is a failing next build. If your next.config contains a webpack() function and you run next build with the default bundler, Next.js stops the build on purpose. Turbopack ignores webpack() configs entirely, so building silently without your customizations could ship a broken site.
You have three options:
- Migrate the webpack customization to the equivalent
turbopackoption. This is the recommended path. - Build with Turbopack anyway using
next build --turbopack, if the webpack config only matters for webpack builds. - Keep webpack with
next build --webpack.
If you never wrote a webpack() function yourself, a plugin probably added one. Wrappers like older versions of MDX, PWA, or bundle analyzer plugins do this. Check what each withSomething() in your config does.
If you need a refresher on what those webpack customizations were doing, see how to customize the webpack configuration in Next.js.
Configuring Turbopack
Turbopack options live under the top-level turbopack key in next.config.ts. The main options are rules, resolveAlias, resolveExtensions, and root.
Importing SVGs as React components
The most common webpack customization is @svgr/webpack. With Turbopack, you register the loader in turbopack.rules and tell Turbopack to treat the output as JavaScript:
npm install --save-dev @svgr/webpack
// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
turbopack: {
rules: {
"*.svg": {
loaders: ["@svgr/webpack"],
as: "*.js",
},
},
},
};
export default nextConfig;
Now SVG files import as components:
// app/components/logo.tsx
import LogoIcon from "./logo.svg";
export function Logo() {
return <LogoIcon width={120} height={32} aria-label="Acme" />;
}
Add a type declaration so TypeScript knows what an SVG import returns:
// svg.d.ts
declare module "*.svg" {
import type { FC, SVGProps } from "react";
const content: FC<SVGProps<SVGSVGElement>>;
export default content;
}
Loaders that need options use the object form:
// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
turbopack: {
rules: {
"*.svg": {
loaders: [{ loader: "@svgr/webpack", options: { icon: true } }],
as: "*.js",
},
},
},
};
export default nextConfig;
Keep these limits in mind: only loaders that return JavaScript are supported, options must be plain serializable values (you cannot pass a require()d plugin as an option), and only a core subset of the webpack loader API is implemented. Loaders known to work include @svgr/webpack, raw-loader, yaml-loader, svg-inline-loader, string-replace-loader, and graphql-tag/loader.
Loading YAML or raw text files
// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
turbopack: {
rules: {
"*.yaml": {
loaders: ["yaml-loader"],
as: "*.js",
},
"*.md": {
loaders: ["raw-loader"],
as: "*.js",
},
},
},
};
export default nextConfig;
Restricting where a loader runs
Rules can carry a condition. Excluding node_modules with the built-in foreign condition is a cheap performance win, and the browser condition lets you transform files differently for client and server code:
// 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;
Other built-in conditions include development, production, and node. You can also match on path, content, and query, and combine conditions with all, any, and not.
Aliases and extensions
resolveAlias replaces webpack.resolve.alias, and resolveExtensions replaces webpack.resolve.extensions:
// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
turbopack: {
resolveAlias: {
underscore: "lodash",
// Map the legacy Sass tilde syntax used by older stylesheets
"~*": "*",
},
resolveExtensions: [".mdx", ".tsx", ".ts", ".jsx", ".js", ".mjs", ".json"],
},
};
export default nextConfig;
You usually do not need resolveAlias for your own path aliases. Turbopack reads paths and baseUrl from tsconfig.json, so aliases like @/components/* work without extra config. When you override resolveExtensions, include the defaults you still need, because the list replaces them rather than adding to them.
Monorepos and linked packages
Turbopack only resolves files inside its root directory. Next.js detects the root by looking for a lockfile (package-lock.json, pnpm-lock.yaml, yarn.lock, or bun.lock). In most monorepos that is correct automatically. If you use npm link to work on a package that lives outside the project, or your lockfile is not at the workspace root, set root explicitly:
// next.config.ts
import path from "node:path";
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
turbopack: {
// Must be an absolute path that contains the app and the linked package
root: path.resolve(process.cwd(), ".."),
},
};
export default nextConfig;
The Filesystem Cache
Turbopack persists its work to disk between runs. In Next.js 16.3, both caches are on by default:
turbopackFileSystemCacheForDevstores dev compilation in.next/dev/cache/turbopack, so restartingnext devreuses previous work.turbopackFileSystemCacheForBuildstores build compilation in.next/cache/turbopack, so repeated builds start warm.
The build cache only helps if .next/cache survives between builds. On a VPS that builds in the same directory, that happens automatically. In CI and Docker, you must cache or mount the directory yourself. A GitHub Actions example:
# .github/workflows/build.yml
- uses: actions/cache@v4
with:
path: |
~/.npm
${{ github.workspace }}/.next/cache
key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**/*.ts', '**/*.tsx') }}
restore-keys: |
${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-
If your build environment never keeps .next/cache, turn the build cache off so Turbopack does not spend time writing files nobody will read:
// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
experimental: {
turbopackFileSystemCacheForBuild: false,
},
};
export default nextConfig;
If you ever suspect a stale cache, stop the dev server and delete .next. It is safe to remove and is regenerated on the next run.
Turbopack-Only Features
Turbopack supports a couple of Vite-style APIs that do not exist under webpack.
import.meta.env exposes build metadata that Turbopack can statically analyze, so dead branches are removed:
// lib/logger.ts
export function debug(message: string) {
if (import.meta.env.DEV) {
console.log(`[debug] ${message}`);
}
}
import.meta.glob imports many modules with one pattern, which is handy for content or plugin registries:
// lib/load-widgets.ts
const widgets = import.meta.glob("./widgets/*.tsx", { import: "default" });
export async function loadWidget(name: string) {
const loader = widgets[`./widgets/${name}.tsx`];
if (!loader) throw new Error(`Unknown widget: ${name}`);
return loader();
}
Both APIs fail if you switch back to webpack with --webpack, so avoid them in projects that still build with webpack.
Behavior Differences When You Switch
Most apps switch with no visible change, but a few differences can surprise you:
- CSS Module ordering follows JS import order. webpack sometimes reordered CSS modules from files it considered side-effect free. If two modules style the same property and you relied on accidental ordering, the winner may change. Fix it by making one stylesheet
@importthe other, or by removing the conflicting rules. - CSS decimal precision. Lightning CSS writes 5 decimal digits where webpack wrote 10, so
line-height: 1.4705882353becomes1.47059. This rarely matters, but pixel-perfect visual tests may flag it. - The Sass
~prefix is not supported. Change@import "~bootstrap/dist/css/bootstrap.min.css";to@import "bootstrap/dist/css/bootstrap.min.css";or add the"~*": "*"alias shown earlier. - webpack plugins do not run. Anything that relies on the webpack plugin API, such as some Sentry, PWA, or code-generation setups, needs a Turbopack-compatible version or must stay on webpack.
sassOptions.functionsis not supported, because Turbopack cannot execute JavaScript functions passed into its Rust-based Sass pipeline.- Legacy CSS Modules features such as standalone
:globalpseudo-classes,@value, and:exportare not supported. Use the:global(...)function form and CSS variables. - Yarn PnP is not supported.
Analyzing Bundles With Turbopack
The @next/bundle-analyzer plugin is built on webpack, so it does not work with Turbopack builds. Since Next.js 16.1, there is an experimental analyzer built for Turbopack:
npx next experimental-analyze
It builds the app and opens an interactive view of the client and server bundles, so you can see which dependencies are inflating each route. Dedicated techniques for trimming what it finds are in how to analyze and reduce JavaScript bundle size in Next.js.
Getting the Most Speed Out of Turbopack
Turbopack is fast, but your project structure still matters:
- Import only what you use from large libraries. Turbopack optimizes barrel imports automatically for many packages, but importing an entire icon set or utility library still means more modules to process.
- Keep loaders off
node_modules. Use theforeigncondition so custom loaders only run on your own files. - Persist
.next/cachein CI. Without it, every build is a cold build. - Leave type checking to
tsc. Runningtsc --noEmitin a separate process or in CI keeps the dev server focused on compiling. - Do not visit every route at startup. Lazy compilation is a feature. Warm-up scripts that crawl the whole app throw away the advantage.
- Check your Tailwind setup. If Tailwind scans far more files than your source folders, such as
node_modulesor the whole repository in a monorepo, CSS compilation slows down no matter which bundler you use. In Tailwind v4, narrow the sources with@sourcerules.
Common Problems and Fixes
- Build stops because a webpack configuration was found. Your config or a plugin defines
webpack(). Migrate it toturbopack, or runnext build --webpack. - "Module not found" for a linked package. The package lives outside the Turbopack root. Set
turbopack.rootto a directory that contains both projects. - SVG import returns an object instead of a component. The
*.svgrule is missingas: "*.js", or the loader is not installed. - Styles look different after switching. Check for CSS Module ordering conflicts and the Sass
~prefix first. - Turbopack is not used on an unusual platform. Turbopack ships native binaries for macOS, Windows, and Linux on x64 and ARM64. Other platforms fall back to WebAssembly bindings without Turbopack, so use
--webpackthere. - Dev server slow or using lots of memory. Generate a trace with
next dev --internal-trace, which writes.next-profiles/trace-turbopack.bin, and attach it to a GitHub issue.
Turbopack FAQ
Yes. Turbopack has been stable for development since Next.js 15 and became the default bundler for both next dev and next build in Next.js 16.
No. In Next.js 16, Turbopack is the default, so the --turbopack flag is redundant. You only need a flag to opt out, which is --webpack.
Many of them. You register loaders in turbopack.rules and map the output to JavaScript. Loaders must return JavaScript, take serializable options, and rely only on the core loader API. Popular loaders such as @svgr/webpack, raw-loader, and yaml-loader are known to work.
No. Turbopack does not implement the webpack plugin API. Look for a Turbopack-compatible alternative, or keep using webpack with the --webpack flag until one exists.
No. Turbopack compiles TypeScript with SWC but does not type-check. Run tsc in watch mode, rely on your editor, or let next build run its type check step.
Yes. The .next folder holds build output and the Turbopack filesystem cache. Deleting it forces a cold compile on the next run but does not affect your source code.
Conclusion
Turbopack makes Next.js development noticeably faster by compiling only what you request, caching its work at a fine grain, and keeping that cache on disk between runs. In Next.js 16, you get it by default for both development and production builds.
For most projects, the work is limited to removing the old --turbopack flag and moving any experimental.turbo options to the top-level turbopack key. Projects with a custom webpack() function should migrate loaders to turbopack.rules, aliases to resolveAlias, and extension lists to resolveExtensions, then check for CSS ordering and Sass differences. Persist .next/cache in CI, keep loaders off node_modules, and use the experimental analyzer to keep bundles lean.
Here are some useful references for going deeper on Turbopack:
- Next.js Docs: Turbopack — supported features, known gaps with webpack, and experimental options.
- Next.js Docs: turbopack config option — rules, conditions, resolveAlias, resolveExtensions, and root.
- Next.js Docs: Upgrading to Version 16 — Turbopack by default and the webpack config build check.
- Next.js Docs: Turbopack FileSystem Caching — how the dev and build caches work.
- Lightning CSS: Lightning CSS documentation — the CSS compiler Turbopack uses under the hood.


