Turbopack & the Compiler
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Next.js 16.3.8 (the latest release on 2026-10-03, when the facts and examples below were
checked; the official documentation reports @doc-version 16.3.8), React 19.3.0, Node.js 20.9.0 or later and
TypeScript 5.1 or later. The toolchain it describes is shared by the App Router and the Pages Router: the same
bundler and the same compiler process both, and the few differences (for instance how transpilePackages behaves for
server code) are called out where they occur. Caching of data and pages is not the topic here, so the examples use the
default previous model of cacheComponents; the build cache this page does cover is the compiler’s own on-disk
cache, a different thing from the Data Cache or the Cache Components caches in Caching.
Every example was built and run with Next.js 16.3.8, Node.js 24.16 and npm 11.13 on macOS, in a scratch copy of the
Bookshelf application that the rest of this section grows, and each one is followed by the official page it derives
from.
It answers a practical question: when you run next dev or next build, what turns TypeScript, JSX, CSS and
node_modules into the JavaScript the server and the browser execute, and which knobs does Next.js give you over that
step? The answer has two layers, and mixing them up is the commonest source of confusion in older material:
-
the compiler (SWC, written in Rust, "the Next.js Compiler") transforms individual files: TypeScript and JSX to JavaScript, React Server Component boundaries, Fast Refresh, down-levelling and minification;
-
the bundler (Turbopack by default, webpack on request) builds the module graph, splits it into chunks and writes the output.
For how the output is then rendered and sent, see Rendering Fundamentals; for where the config keys live, Configuration, TypeScript and Environment Variables; for bundle size, Client-Side Rendering and Performance and Web Vitals.
The Toolchain at a Glance
Five tools can sit between your source and the output. Which ones run depends on the bundler flag and on whether the project carries a Babel configuration file.
| Tool | Role | Default in 16.3 |
|---|---|---|
SWC (the Next.js Compiler) |
Per-file transforms: TypeScript, JSX, ESNext down-levelling, minification, Next.js-specific transforms (server/client boundaries, |
Always used for Next.js’s own transforms, under both bundlers. Replaces Babel and Terser. |
Turbopack |
Bundler: one unified graph for client, server and Edge, incremental computation cached on disk, lazy bundling in dev. |
The default for |
webpack |
The previous bundler. A |
Opt-in with |
Babel |
Only for your own custom transforms (a |
Not used unless a Babel config file exists (or |
Rspack |
A webpack-compatible Rust bundler, through the community |
Not used; experimental and community supported. |
Source: Turbopack and Next.js Compiler
The next diagram shows the decision for one source file. The point to retain is that SWC is always on the path under Turbopack, even when Babel is added, while with webpack a Babel config file switches SWC off for the whole project.
.ts .tsx .js .css"] --> BC{"Babel config
file present?"} BC -- "no" --> SWC["SWC
TypeScript, JSX, Next.js transforms,
compiler.* options, down-levelling"] BC -- "yes, Turbopack" --> BL["babel-loader
your plugins and presets"] --> SWC BC -- "yes, webpack" --> BO["Babel only
SWC disabled,
compiler.* options ignored"] SWC --> LD{"turbopack.rules
match the file?"} LD -- "yes" --> LDR["webpack-style loaders
then module type"] LD -- "no" --> GR LDR --> GR["Module graph
client, server, Edge in one graph"] BO --> WG["webpack module graph
three compilations"] GR --> CH["Chunks and output in .next"] WG --> CH
Turbopack
Turbopack is an incremental bundler written in Rust and built into Next.js. The official documentation gives four reasons for its design:
-
One unified graph. Next.js has several output environments (client, Node.js server, Edge). webpack runs a separate compilation per environment (the
webpackconfig function is called three times) and stitches the results together; Turbopack models all of them in a single graph. -
Bundling in dev, not native ESM. It bundles during development, in an optimised way, instead of sending thousands of unbundled modules to the browser.
-
Incremental computation. Work is parallelised across cores and cached at the function level; results persist to disk between runs (see The File-System Cache and Memory Eviction).
-
Lazy bundling. In dev it only bundles what the dev server is asked for.
Turbopack does not type-check. TypeScript errors come from the separate "Running TypeScript" step of next build and
from your editor (the build output below shows both steps).
Default since 16, and the opt-out
Turbopack became the default bundler for both next dev and next build in Next.js 16.0 (2025-10-21). It reached
next dev stability in 15.0, shipped next build as experimental in 15.3 and as beta in 15.5. A project needs no
configuration and no flag:
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
}
}
Source: Turbopack: getting started
The banner of every command names the bundler, which is the quickest way to know which one ran:
next build in a 16.3.8 project▲ Next.js 16.3.8 (Turbopack)
- Environments: .env.local
✓ Running next.config.ts took 665ms
Creating an optimized production build ...
✓ Compiled successfully in 1802ms
Running TypeScript ...
Finished TypeScript in 811ms ...
Source: Turbopack
The CLI flags are --turbopack (alias --turbo) and --webpack, on both next dev and next build. Since 16 the
--turbopack flag is redundant; it is only useful to override a custom webpack config (see below). Turbopack needs
native bindings, which exist for macOS, Windows and Linux (glibc and musl) on x64 and ARM64. On platforms without them
(FreeBSD, OpenBSD) Next.js falls back to WebAssembly bindings, which support compilation and minification but not
Turbopack, so you must pass --webpack there.
To keep webpack, either for one command or for the whole project:
{
"scripts": {
"dev": "next dev --webpack",
"build": "next build --webpack",
"start": "next start"
}
}
The upgrade guide also shows the split setup, Turbopack in development and webpack for production builds
("dev": "next dev", "build": "next build --webpack"), and asks people who cannot move to Turbopack to say why in the
linked discussion thread. Mixing the two means that development and production can behave differently; prefer one
bundler for both.
A custom webpack config fails the build
A project that has a webpack function in next.config and runs a plain next build does not silently ignore it.
Turbopack refuses to build, to stop you from shipping something that depends on a config it will not apply. This is the
first thing that happens when you upgrade an older project, and it is also what happens when a plugin you installed adds
a webpack function behind your back. Reproduced with Next.js 16.3.8:
webpack function, built with the default bundlerimport type { NextConfig } from "next";
const nextConfig: NextConfig = {
webpack: (config) => {
config.module.rules.push({ test: /\.txt$/, type: "asset/source" });
return config;
},
};
export default nextConfig;
$ npx next build
▲ Next.js 16.3.8 (Turbopack)
...
⨯ ERROR: This build is using Turbopack, with a `webpack` config and no `turbopack` config.
This may be a mistake.
As of Next.js 16 Turbopack is enabled by default and
custom webpack configurations may need to be migrated to Turbopack.
...
Many applications work fine under Turbopack with no configuration,
if that is the case for you, you can silence this error by passing the
`--turbopack` or `--webpack` flag explicitly or simply setting an
empty turbopack config in your Next config file (e.g. `turbopack: {}`).
Three ways out, all verified: next build --webpack (the config is applied and the banner says (webpack)),
next build --turbopack (the webpack function is ignored), or an empty turbopack: {} next to it (the error goes
away and Turbopack builds, again ignoring webpack). The right fix is the third path with real content: port what the
webpack function did to the turbopack options in the next section.
What works out of the box
Turbopack is zero-configuration for the common cases, and the official page lists what is supported:
| Area | Status |
|---|---|
JavaScript, TypeScript, JSX/TSX |
Supported through SWC. No type checking in the bundler. |
ESM and CommonJS |
Static and dynamic |
React Server Components, Fast Refresh |
Supported, no configuration. Automatic creation of a missing root layout is not (Turbopack tells you to create it). |
CSS, CSS Modules, nesting, |
Supported; Lightning CSS compiles it. PostCSS config files ( |
Sass and SCSS |
Supported out of the box ( |
Images, fonts, JSON |
|
Path aliases |
|
Babel |
Detected automatically since 16: if a Babel config file exists, |
Magic comments |
|
Source: Turbopack: supported features
Configuring Turbopack
Turbopack options live under the top-level turbopack key of next.config.ts. In 15.2 and earlier the key was
experimental.turbo (still accepted as an alias); the codemod npx @next/codemod@latest next-experimental-turbo-to-turbopack .
migrates it. The 16 documentation lists five options (plus ignoreIssue, which has its own page):
| Option | Purpose |
|---|---|
|
The application root, an absolute path. Turbopack only resolves files inside it. Next.js finds it by looking for a lockfile ( |
|
Webpack-style loaders (and module types) applied by file glob. |
|
Map an import specifier to another module, like |
|
The list of extensions tried when resolving imports. It replaces the default list, so include the defaults ( |
|
Emit debug IDs in JavaScript bundles and source maps (read at runtime from |
|
Hide known Turbopack errors and warnings from the CLI and the overlay, matched by |
Source: turbopack and turbopack.ignoreIssue
Loaders (rules)
Turbopack needs no loaders for built-in features, but many webpack loaders already work with it when you need more. A
rule maps a glob to a list of loaders and says what the output is (as). A glob without / matches the file name; one
with / matches the project-relative path. Rules run in order. The loader below is a complete webpack-style loader: it
receives the file source and returns JavaScript.
// A webpack-style loader: receives the file source, returns JavaScript.
module.exports = function textLoader(source) {
return `export default ${JSON.stringify(source.trim())};`
}
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
turbopack: {
rules: {
"*.txt": {
// Only our own code, never node_modules ("foreign").
condition: { not: "foreign" },
loaders: ["./loaders/text-loader.js"],
as: "*.js",
},
},
// Replace one module specifier with another.
resolveAlias: { "legacy-slug": "./lib/slugify.ts" },
},
};
export default nextConfig;
.txt file and the aliased moduleimport slugify from "legacy-slug"; // resolved by turbopack.resolveAlias
import intro from "../../../content/intro.txt"; // transformed by the *.txt rule
export function GET() {
return Response.json({
intro,
slug: slugify("Do Androids Dream of Electric Sheep?"),
});
}
TypeScript does not know either module, so the project declares them (a loader changes what an import evaluates to; the types are your job):
declare module "*.txt" {
const content: string;
export default content;
}
declare module "legacy-slug" {
const slugify: (title: string) => string;
export default slugify;
}
next build passes and next start serves (the response is part of the output of the combined example in
import.meta.glob and import.meta.env):
curl -s localhost:3803/api/tooling
{
"intro": "Dune: a desert planet and a spice.",
"slug": "do-androids-dream-of-electric-sheep"
}
The documented limits of the loader support matter when you port a webpack setup:
-
only a core subset of the loader API is implemented, through the
loader-runnerlibrary;importModule,loadModule,emitFile,utils,resolve(usegetResolve),version,modeandtargetare not available, andfsonly offersreadFile; -
only loaders that return JavaScript are supported; loaders that turn stylesheets or images into other assets are not;
-
loader options must be plain primitives, objects and arrays (no `require()`d plugin modules);
-
the loaders the documentation lists as tested are
babel-loader,@svgr/webpack,svg-inline-loader,yaml-loader,string-replace-loader,raw-loader,sass-loaderandgraphql-tag/loader.
Beyond a plain glob a rule accepts condition with all, any and not and the operators path, content, query
and contentType (16.2), and the built-in conditions browser, foreign, development, production, node and
edge-light. Restricting a loader with { not: "foreign" }, as above, avoids running it over node_modules.
A rule can also set a module type (asset, ecmascript, typescript, css, css-module, wasm, raw, bytes,
added in 16.2) with or without loaders.
There is a per-import form too, using import attributes (16.2): turbopackLoader, turbopackLoaderOptions (a JSON
string), turbopackAs and turbopackModuleType. It is Turbopack-only and needs with, not assert. The loader path
is resolved relative to the importing file:
// The loader path is resolved relative to the importing file.
import raw from "../../../content/raw.data" with { turbopackLoader: "../../../loaders/text-loader.js", turbopackAs: "*.js" };
export function GET() {
return Response.json({ raw });
}
{ "raw": "raw text from a file" }
In the scratch app, a project-root-relative loader path ("./loaders/text-loader.js") in the same attribute made
Turbopack panic with Unable to resolve turbopackLoader instead of reporting a normal error, so use a path relative to
the importing file or a package name.
Aliases for code that should not be in the browser
A frequent webpack setting is resolve.fallback to silence "Module not found: Can’t resolve 'fs'" from a dependency that
imports Node.js built-ins in code that also reaches the browser. The Turbopack equivalent is a conditional alias: only
the browser condition is supported, and the usual target is an empty module. Without it, the build stops:
fs in a Client Component, no aliasError: Module not found: Can't resolve 'fs'
> 4 | import * as fs from "fs";
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
turbopack: {
resolveAlias: {
// Prefer fixing the import: this only silences the browser build.
fs: { browser: "./empty.ts" },
},
},
};
export default nextConfig;
With empty.ts containing export {}, the same next build compiles. The upgrade guide recommends refactoring so that
client code never imports modules that need Node.js built-ins, and using the alias only when that is impossible. The same
mechanism covers the old Sass tilde syntax: webpack accepted @import '~bootstrap/…', Turbopack does not, and
resolveAlias: { "~": "" } restores it if you cannot edit the imports.
import.meta.glob and import.meta.env
Turbopack supports import.meta.glob() (new in 16.3), a Vite-compatible API that imports many modules by glob. The result is an
object keyed by the path relative to the calling file. By default each value is a thunk returning a Promise for the
module (lazy, one chunk each); eager: true bundles them up front and gives the module objects directly; import
selects one named export; query appends a query string to every import (?raw, ?url); an array of patterns can
carry ! negations; caseSensitive and base complete the options. The as option and import.meta.globEager() are
not supported. It is not available with webpack. TypeScript types are shipped when moduleResolution is bundler,
node16 or nodenext (the default for new projects).
import.meta.env is also available with Turbopack only: DEV, PROD, MODE, BASE_URL (the basePath with a
trailing slash) and SSR (true in server bundles), statically analysed so dead branches disappear. Custom VITE_*
variables are not supported; for your own values use the environment variables of
Configuration, TypeScript and Environment
Variables.
import slugify from "legacy-slug"; // resolved by turbopack.resolveAlias
import intro from "../../../content/intro.txt"; // transformed by the *.txt rule
// Lazy (default): each value is a function returning a Promise for the module.
const parts = import.meta.glob("./parts/*.ts");
// Eager: modules are bundled up front; `import` picks one named export.
const titles = import.meta.glob("./parts/*.ts", { eager: true, import: "title" });
// `base` reaches outside the calling file's folder.
const notes = import.meta.glob("*.txt", {
base: "../../../content/notes",
eager: true,
import: "default",
});
export async function GET() {
const first = await parts["./parts/a.ts"]();
return Response.json({
intro,
slug: slugify("Do Androids Dream of Electric Sheep?"),
partKeys: Object.keys(parts),
first,
titles,
notes,
mode: import.meta.env.MODE,
dev: import.meta.env.DEV,
});
}
With app/api/tooling/parts/a.ts and b.ts exporting title and a default, and two .txt files under content/notes,
next build && next start answers:
{
"intro": "Dune: a desert planet and a spice.",
"slug": "do-androids-dream-of-electric-sheep",
"partKeys": ["./parts/a.ts", "./parts/b.ts"],
"first": { "default": "a", "title": "Part A" },
"titles": { "./parts/a.ts": "Part A", "./parts/b.ts": "Part B" },
"notes": {
"../../../content/notes/dune.txt": "Dune notes: ecology matters.",
"../../../content/notes/solaris.txt": "Solaris: an ocean that thinks."
},
"mode": "production",
"dev": false
}
The same route under next dev reports "mode": "development" and "dev": true.
One behaviour is not in the documentation and is worth knowing: in this build a pattern that climbs out of the calling
file’s folder ("../../../content/notes/*.txt") matched nothing and returned an empty object without any error, while
the same files matched when the climb was moved into base. Keep the files below the calling module, or use base, and
check that the result is not empty.
The File-System Cache and Memory Eviction
Turbopack caches its compiler artifacts to disk and reuses them on the next run. Two flags control it, one for each
command, and both live under experimental:
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
experimental: {
turbopackFileSystemCacheForDev: true,
turbopackFileSystemCacheForBuild: true,
turbopackMemoryEviction: "auto",
},
};
export default nextConfig;
| Option | Default | Where it writes, and since when |
|---|---|---|
|
|
|
|
|
|
|
|
Dev only. |
Source: turbopackFileSystemCache and turbopackMemoryEviction
Memory eviction follows from the cache: once Turbopack has persisted a snapshot to disk, it can drop the in-memory copy and
reload it on demand, which is why the option only has an effect in next dev with the file-system cache on. The 16.3
release post reports up to 90 percent less memory in long dev sessions (an example in the post goes from 21.5 GB to about
2 GB on a very large application). Treat the figure as the vendor’s measurement on a large codebase, not a promise. When
you set an experimental option explicitly, such as turbopackMemoryEviction, next build lists it under "Experiments (use
with caution)", which is a quick way to see that the key was read:
▲ Next.js 16.3.8 (Turbopack)
- Environments: .env.local
✓ Running next.config.ts took 11ms
- Experiments (use with caution):
· turbopackMemoryEviction: "auto"
The scratch Bookshelf is tiny, so absolute times are small, but the effect of the cache is visible. The "Compiled
successfully" line of next build, on one machine, with the cache directory removed before the first run:
| Setup | First build (cold) | Second build (warm) |
|---|---|---|
Turbopack, file-system cache on (default) |
2.0 s |
0.14 s and 0.16 s (two runs) |
Turbopack, |
1.5 s |
1.7 s |
webpack ( |
3.9 s |
1.2 s (webpack has its own cache in |
In development the same effect shows as a faster first request after a restart: the first request to /en/books/1 took
1.8 s on a cold .next and 0.54 s after stopping and restarting next dev. The cache directory was 52 MB after one
Turbopack build and 120 MB after one webpack build of this app, which is a reason to check disk limits in CI.
The build cache only helps if .next/cache survives between builds. On a self-hosted server that means reusing the
working directory; in a container it means caching or mounting the directory; on a CI provider it means configuring the
provider’s cache for .next/cache. If your environment never preserves it, set turbopackFileSystemCacheForBuild: false
so Turbopack does not write a cache that will never be read.
Source: CI build caching
Other experimental flags
The same experimental object carries the rest of Turbopack’s switches, all marked experimental in the documentation.
The table lists the ones that change what you ship; the Turbopack reference has the full list with the defaults for dev
and build.
| Option | Default | Effect |
|---|---|---|
|
|
Minification of the output (off in dev). |
|
|
Tree shaking of unused exports and imports (off in dev; the imports flag requires the exports flag). |
|
|
Scope hoisting; always off in dev. |
|
|
The module id strategy. |
|
follows |
Source map generation. |
|
see below |
Size thresholds of the production chunker: |
|
|
Resolve |
|
not set |
Run the native Rust port of the React Compiler inside Turbopack instead of the Babel transform. Needs |
Source: Turbopack: configuration and turbopackChunking
For performance or memory problems, next dev --internal-trace writes .next-profiles/trace-turbopack.bin, the file the
Next.js team asks for in an issue. next build --debug-build-paths "app/**" builds a subset of routes, and
next build --experimental-analyze runs the Turbopack bundle analyzer (experimental, Turbopack only; the older
@next/bundle-analyzer plugin is for webpack). The analyzer is covered in
Client-Side Rendering.
Gaps Against webpack
The official page lists "known gaps" and unsupported features. Together with what was observed in the scratch app, this is the checklist for a migration. None of it matters for a new project that sticks to the built-in features.
| Area | Turbopack behaviour |
|---|---|
|
Not applied, and a plain |
webpack plugins |
Not supported at all. Loaders work (the subset above); plugins that hook the webpack compiler do not. |
Filesystem root |
Files outside the project root are not resolved, which breaks |
CSS Module ordering |
Follows JavaScript import order. webpack sometimes ignored it (for files it inferred as side-effect free), so an app that relied on an accidental order can render differently. Fix by making the dependency explicit with |
Sass |
No |
CSS precision |
Lightning CSS keeps 5 decimal digits, webpack 10: |
Legacy CSS Modules features |
No standalone |
Unsupported options |
Yarn Plug’n’Play, |
Babel |
Both bundlers can run it, but differently (see Babel, when you really need it). |
Vite-style APIs |
|
The last row was verified: the scratch route that uses import.meta.glob and an alias fails under next build --webpack
(Module not found: Can’t resolve 'legacy-slug' for the alias, because the alias is a Turbopack option) and builds under
Turbopack. A project that must be able to build with both bundlers therefore has to stay inside the shared subset.
The SWC Next.js Compiler
The Next.js Compiler is written in Rust on top of SWC. It replaces Babel for individual files and Terser for minification, and it has been on by default since Next.js 12. The official page cites ~3x faster Fast Refresh and ~5x faster builds from the switch to SWC and a 17x speed-up of compilation over Babel (vendor figures, not measured here). SWC has a WebAssembly build, which is what lets Next.js run on platforms without native bindings, with the Turbopack exception noted earlier.
The compiler runs under both bundlers. What it does, grouped by the question you are asking:
-
Is my TypeScript and JSX handled? Yes, with no configuration.
tsconfig.jsonsettings the compiler reads includejsxImportSourceandexperimentalDecorators(legacy decorators, kept for old libraries such as MobX; not recommended for new code). Type checking is a separate step. -
Is my code minified? Yes, by SWC in production, 7x faster than Terser by the page’s own figure, since 13.0 (stable in 12.3). Since 15 the minifier is no longer configurable: the
swcMinifyflag was removed. -
Can I strip or replace things at build time? Yes, through the
compilerobject, below. -
Does it support my CSS-in-JS or GraphQL library? The compiler has built-in ports of
babel-plugin-styled-components(compiler.styledComponents),@emotion/babel-plugin(compiler.emotion) and Relay (compiler.relay). Check the library’s own documentation for the supported setup with Server Components. -
Does Jest use it?
next/jestsets up the SWC transform, mocks CSS and image imports, loads.envfiles and ignoresnode_modulesand.next. See the testing guidance of the section; the sibling page on testing shows it in use.
The compiler options
The compiler key of next.config switches on SWC transforms that otherwise need a Babel plugin. The four below
are the ones used most often. The configuration shows all of them together; the route and the page after it show the
effect, which can be checked in the build output and in the HTML.
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
compiler: {
// Remove console.* calls from application code (not node_modules), except console.error.
removeConsole: { exclude: ["error"] },
// Remove matching JSX props. The pattern is a Rust regex.
reactRemoveProperties: { properties: ["^data-testid$"] },
// Statically replace identifiers in client, server and Edge code.
define: { BUILD_LABEL: "bookshelf-2026-10-03" },
// The same, but only for server and Edge code.
defineServer: { "process.env.FEATURE_X": "on" },
},
};
export default nextConfig;
declare const BUILD_LABEL: string; // replaced at build time by compiler.define
export function GET() {
console.log("MARK_LOG debug trace"); // removed by compiler.removeConsole
console.error("MARK_ERR kept on purpose"); // kept: excluded from removal
return Response.json({
label: BUILD_LABEL,
flag: process.env.FEATURE_X, // replaced by compiler.defineServer
});
}
export default function Page() {
return (
<button data-testid="buy" data-custom="keep-me" data-test-other="x">
Buy
</button>
);
}
After next build, the marker MARK_LOG is absent from every JavaScript file in .next/server while MARK_ERR is
present; next start prints only the error line; and the two requests return:
curl -s localhost:3803/api/compiler
curl -s localhost:3803/en/swc-demo | grep -o '<button[^>]*>'
{"label":"bookshelf-2026-10-03","flag":"on"}
<button data-custom="keep-me" data-test-other="x">
Only data-testid was removed, because the regular expression is anchored; the default pattern of
reactRemoveProperties: true is ^data-test, which would also have removed data-test-other. The same removal happens
under next dev, so a console.log that vanishes only in a built app is not a bug in your logic: check this option first.
Two details are easy to get wrong. compiler.define and compiler.defineServer take the replacement as source text
that is inserted into the code: the value "bookshelf-2026-10-03" above became the string content, but wrapping it in
JSON.stringify(…), as one would for webpack’s DefinePlugin, produced the string with an extra pair of quotes
("\"bookshelf-2026-10-03\"") in the scratch app. And the build-time replacements are not the way to expose configuration
to the browser; use NEXT_PUBLIC_ environment variables for that and keep secrets out of any replacement, because the
replaced value is written into the bundle.
Two more compiler hooks exist. compiler.runAfterProductionCompile is an async function called after the production
compilation and before type checking and static page generation, receiving { distDir, projectDir }; third-party tools use
it to collect source maps. experimental.swcTraceProfiling: true writes swc-trace-profile-<timestamp>.json under
.next/, loadable in Chromium’s trace viewer or Perfetto. experimental.swcPlugins loads SWC plugins written in
WebAssembly, as [path-or-package, options] tuples; the feature has been experimental since 12.2.
Source: Next.js Compiler
Babel, when you really need it
A project that has a Babel configuration file (.babelrc or babel.config.js) gets different behaviour from each bundler,
and the difference decides whether compiler.* still works.
-
Turbopack (16 and later) keeps SWC for the Next.js transforms and for down-levelling, and adds
babel-loaderfor the files in your project (notnode_modules, unless you configure the loader yourself). The build printsUsing external babel configuration from <project>/.babelrc. -
webpack disables SWC for the project:
Disabled SWC as replacement for Babel because of custom Babel configuration, followed bycompiler options in next.config.js will be ignored while using Babel(the pagesswc-disabledandignored-compiler-optionsunder nextjs.org/docs/messages explain both).
A Babel config must include the Next.js preset, because Babel alone cannot parse TypeScript. The smallest useful setup (and the one that was run) is a preset plus a local plugin:
{
"presets": ["next/babel"],
"plugins": ["./babel-plugin-uppercase-marker.js"]
}
// Replaces the identifier __BABEL_MARK__ with a string literal.
module.exports = function ({ types: t }) {
return {
visitor: {
Identifier(path) {
if (path.node.name === "__BABEL_MARK__" && path.isReferencedIdentifier()) {
path.replaceWith(t.stringLiteral("rewritten-by-babel"));
}
},
},
};
};
With declare const BABEL_MARK: string; in a route that returns it, next build prints the "external babel
configuration" line and curl returns "rewritten-by-babel". A first attempt with a .babelrc that listed only
"plugins": [] failed with a TypeScript syntax error ("Unexpected token, expected ","") in a .tsx file, which is what
happens without next/babel. Under --webpack the same project prints the SWC-disabled message and ignores the
compiler block of the config.
The React Compiler is the other Babel user: reactCompiler: true (stable and off by default in 16) runs it through
Babel, which the upgrade guide warns makes dev and build slower; experimental.turbopackRustReactCompiler: true runs a
native Rust port inside Turbopack instead, only with Turbopack, and is experimental. See
Performance and Web Vitals for what the compiler does to your components.
Transpiling Packages
Next.js does not compile code under node_modules by default: it expects dependencies to ship plain JavaScript. A package
that ships raw TypeScript or JSX, or modern syntax your browser targets cannot run, needs to be opted in with
transpilePackages, which lists package names (including scoped names; no paths and no globs). The option appeared in
13.0 and replaces the next-transpile-modules package.
The experiment uses a stand-in for such a dependency: node_modules/@acme/ui, with a package.json whose main is
index.ts and a TypeScript file that has an interface and a function.
export interface Price { cents: number; currency: "EUR" | "USD" }
export function formatPrice(p: Price): string {
return `${(p.cents / 100).toFixed(2)} ${p.currency}`;
}
import { formatPrice } from "@acme/ui";
export function GET() {
return Response.json({ price: formatPrice({ cents: 1999, currency: "EUR" }) });
}
Without any configuration, Turbopack stops at the first file of the package:
Error: Turbopack build failed with 1 error:
./node_modules/@acme/ui/index.ts
Error: Unknown module type
This module doesn't have an associated type. Use a known file extension, or register a loader for it.
and webpack fails with Module parse failed: Unexpected token (1:7) on the same file. Listing the package fixes both:
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
// Compile a dependency that ships raw TypeScript.
transpilePackages: ["@acme/ui"],
};
export default nextConfig;
npx next build && npx next start
curl -s localhost:3803/api/price
{"price":"19.99 EUR"}
The same response comes from next build --webpack with the same config.
| Situation | What to do |
|---|---|
A dependency in |
Add it to |
A workspace package of your monorepo (npm, pnpm or Yarn workspaces) |
Nothing for Turbopack under either router. webpack does the same for the App Router. In the scratch app a symlinked package inside the project, with raw TypeScript, built under both bundlers without the option, consistent with the documentation. |
Pages Router with webpack, and the package’s source is outside the app folder (for example |
Add it to |
Pages Router, and you want a |
The Pages Router loads such dependencies through Node.js |
The package must stay external on the server |
Use |
A large library with many modules (icon sets and similar) |
Consider |
Rspack (Community)
Rspack is a Rust bundler with a webpack-compatible API, from a different team (the Rspack project),
which published next-rspack, a plugin for projects that depend on webpack features Turbopack does not offer (for
example webpack plugins) and want a faster build without moving to Turbopack. The official Next.js documentation lists
it under "Community" and marks it experimental and not recommended for production. It is not an official Next.js
plugin; feedback goes to the discussion thread the plugin links. The Rspack team’s guide asks for Next.js 15.3 or later.
Install the package and wrap the config. Do not pass --webpack or --turbopack; the plugin switches the bundler itself.
import withRspack from "next-rspack";
import type { NextConfig } from "next";
const nextConfig: NextConfig = {};
export default withRspack(nextConfig);
npm add -D next-rspack
npx next build
next build with next-rspack 16.3.8 (abridged)▲ Next.js 16.3.8 (Turbopack)
- Environments: .env.local
✓ Running next.config.ts took 10ms
✓ Switching bundler from Turbopack to Rspack based on config
Creating an optimized production build ...
⚠ `next-rspack` is currently experimental. It's not an official Next.js plugin, and is supported by the Rspack
team in partnership with Next.js. ...
✓ Compiled successfully in 1423ms
Notice that the banner still says "Turbopack": it is printed before the plugin switches the bundler, and the line that
follows tells the truth. next dev prints the same "Switching bundler" line and served pages normally. The customisation
API is the webpack one: the webpack function of next.config receives the Rspack config, as the Rspack guide describes.
Two behaviours of the 16.3.8 plugin, both reproduced and both with a misleading message:
-
next build --webpack(or--turbopack) together withwithRspackexits withCannot call withRspack and pass the --webpack flag. Please configure only one bundler. -
a plain
next startof the Rspack build exited at start with the same text. Reading the plugin’sindex.js, it only accepts being loaded when the CLI has setTURBOPACK=auto(whichnext buildandnext devdo andnext startdoes not) or whenNEXT_RSPACKis already set. Starting the server withNEXT_RSPACK=true next startworked (the books API and a statically generated page answered 200). If you adopt the plugin, test the production start command, not only the build, and follow the plugin’s issue tracker.
An aside on a documentation link: the Rspack guide the Next.js page links to moved; the current page is under
rspack.rs/guide/integrations/next.
Source: Rspack (community) and Rspack: Next.js integration
Pitfalls and Errors
| Symptom | Cause and fix |
|---|---|
|
A |
|
Refactor the import out of the client path; as a stop-gap alias |
|
The dependency ships TypeScript or JSX: add it to |
A |
|
Compiled output ignores |
A Babel config exists and the project builds with webpack, which disables SWC. Remove the Babel file, or use Turbopack. |
Layout shifts or CSS order changed after moving to Turbopack |
CSS Module ordering follows import order, and decimals are rounded to 5 digits. Make the order explicit with |
A linked package outside the repository is not found |
Turbopack does not resolve outside its root. Set |
|
The pattern climbs out of the calling file. Use |
Builds are not faster in CI |
|
Panic: |
An import-attribute loader path is not relative to the importing file. Use a path relative to it, or a package name. |
A |
The Babel config lacks |
What Changed Recently
| Version | Change |
|---|---|
12.0 |
The Next.js Compiler (SWC) is introduced and enabled by default. A Babel config file opts a project out. |
12.1-12.3 |
Built-in transforms for styled-components, Jest, Relay, remove-console and remove-react-properties (12.1); SWC plugins, experimental (12.2); the SWC minifier becomes stable (12.3). |
13.0 |
The SWC minifier is the default. |
13.5 |
|
15.0 |
Turbopack for |
15.3-15.5 |
Turbopack for |
16.0 (2025-10-21) |
Turbopack is the default for |
16.1 (2025-12-18) |
The file-system cache for dev is on by default. |
16.2 (2026-03-18) |
|
16.3 (2026-08-03) |
The file-system cache for build is on by default; Turbopack memory eviction (experimental) reduces dev memory by up to 90 percent in the release post’s measurements; |
Where the Books Are Outdated
The two requester-provided books are listed in the section bibliography. The compiler and bundler material in them predates Next.js 16, and the current state differs in these ways (no text from the books is reproduced here):
-
Babel as the compiler. The first book (Packt, 2022, written before the App Router and before Turbopack) treats Babel as the tool that compiles Next.js code, including TypeScript through
@babel/plugin-transform-typescriptwith its caveats (noconst enum, noexport =), and shows customising it with a.babelrcthat contains thenext/babelpreset plus plugins. Since 12 the default compiler is SWC and TypeScript is compiled by it, so those Babel-specific caveats no longer apply; a.babelrcstill works but is a deliberate opt-in, and with webpack it disables SWC and thecompileroptions (with Turbopack it is added next to SWC). -
webpack as the bundler. The same chapter describes webpack as the bundler and customises it with the
webpackfunction innext.config.jsfor CSS preprocessors. In 16 Turbopack is the default; awebpackfunction fails a plainnext build, and Sass and CSS need no customisation (see Styling). -
Old browsers. The Babel explanation uses Internet Explorer 10 and 11 as the reason to transpile. Current Next.js targets Chrome, Edge and Firefox 111 and Safari 16.4; Internet Explorer is not supported.
-
Missing topics. Neither book covers Turbopack, the
turbopackconfig, the file-system cache,transpilePackages,compiler.*options or Rspack. The second book (Apress, 2023) mentions webpack only in passing (a stack trace and a remark on chunking). -
process.browser. The first book uses it to branch on the environment; it was removed, andtypeof windowchecks oruse clientboundaries are the current tools (see Client-Side Rendering).
Summary
-
Next.js 16 has two layers: SWC transforms files, Turbopack bundles. Turbopack is the default for dev and build; the banner of every command tells you which bundler ran.
-
Configure Turbopack under
turbopack(rules,resolveAlias,resolveExtensions,root,debugIds,ignoreIssue). Awebpackfunction is not applied and fails a plainnext build. -
Keep the file-system cache on and make
.next/cachepersistent in CI; memory eviction is automatic in dev. -
import.meta.globandimport.meta.envare Turbopack-only. -
The
compileroptions replace Babel plugins; a Babel config file changes how both bundlers behave. -
transpilePackagesis for dependencies that ship uncompiled code. Rspack is a community, experimental alternative.