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.

Table 1. Which tool does what in Next.js 16.3
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, next/font, next/dynamic, compiler.* options), Fast Refresh.

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 next dev and next build since 16.0.

webpack

The previous bundler. A webpack function in next.config customises it.

Opt-in with --webpack. Maintained, but new work targets Turbopack.

Babel

Only for your own custom transforms (a .babelrc or babel.config.js), and for the React Compiler.

Not used unless a Babel config file exists (or reactCompiler is on).

Rspack

A webpack-compatible Rust bundler, through the community next-rspack plugin.

Not used; experimental and community supported.

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.

flowchart TD SRC["Source file
.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
Figure 1. How a source file reaches the output

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 webpack config 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:

package.json scripts for a Next.js 16 project
{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start"
  }
}

The banner of every command names the bundler, which is the quickest way to know which one ran:

The first lines of 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:

Opting out of Turbopack
{
  "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:

A config with a webpack function, built with the default bundler
import 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:

Table 2. Built-in support (Next.js 16.3, from the Turbopack reference)
Area Status

JavaScript, TypeScript, JSX/TSX

Supported through SWC. No type checking in the bundler.

ESM and CommonJS

Static and dynamic import, and require().

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, @import

Supported; Lightning CSS compiles it. PostCSS config files (postcss.config.js, .mjs, .cjs, .ts, .mts, .cts) run in a Node.js worker pool, which is how Tailwind CSS works.

Sass and SCSS

Supported out of the box (sass-loader is configured automatically). Custom Sass functions (sassOptions.functions) are not.

Images, fonts, JSON

import img from './img.png' works, and returns an object for next/image.

Path aliases

paths and baseUrl from tsconfig.json are read.

Babel

Detected automatically since 16: if a Babel config file exists, babel-loader is set up for your code (not for node_modules).

Magic comments

webpackIgnore: true works in both bundlers; turbopackIgnore: true and turbopackOptional: true are Turbopack-only.

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):

Table 3. Options of the turbopack key
Option Purpose

root

The application root, an absolute path. Turbopack only resolves files inside it. Next.js finds it by looking for a lockfile (pnpm-lock.yaml, package-lock.json, yarn.lock, bun.lock, bun.lockb). Set it yourself for a layout without workspaces, or when you npm link packages that live outside the project.

rules

Webpack-style loaders (and module types) applied by file glob.

resolveAlias

Map an import specifier to another module, like resolve.alias in webpack.

resolveExtensions

The list of extensions tried when resolving imports. It replaces the default list, so include the defaults (.tsx, .ts, .jsx, .js, .json…​).

debugIds

Emit debug IDs in JavaScript bundles and source maps (read at runtime from globalThis._debugIds).

ignoreIssue

Hide known Turbopack errors and warnings from the CLI and the overlay, matched by path (glob or RegExp) and optionally title and description. Turbopack only.

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.

loaders/text-loader.js
// A webpack-style loader: receives the file source, returns JavaScript.
module.exports = function textLoader(source) {
  return `export default ${JSON.stringify(source.trim())};`
}
next.config.ts, a rule, an alias and a condition
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;
A route that imports a .txt file and the aliased module
import 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):

types.d.ts
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-runner library; importModule, loadModule, emitFile, utils, resolve (use getResolve), version, mode and target are not available, and fs only offers readFile;

  • 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-loader and graphql-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:

A loader applied to a single import, with no global rule
// 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:

Importing fs in a Client Component, no alias
Error: Module not found: Can't resolve 'fs'
> 4 | import * as fs from "fs";
next.config.ts, an empty module for the browser build
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.

app/api/tooling/route.ts
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:

next.config.ts, the defaults written out
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  experimental: {
    turbopackFileSystemCacheForDev: true,
    turbopackFileSystemCacheForBuild: true,
    turbopackMemoryEviction: "auto",
  },
};

export default nextConfig;
Table 4. The cache options (Next.js 16.3)
Option Default Where it writes, and since when

turbopackFileSystemCacheForDev

true

.next/dev/cache/turbopack. Beta in 16.0, default for dev in 16.1.

turbopackFileSystemCacheForBuild

true

.next/cache/turbopack. Default for builds in 16.3.

turbopackMemoryEviction

'auto'

Dev only. false never evicts, 'auto' evicts once enough memory was allocated since the last eviction (using operating-system memory-pressure feedback), 'full' evicts everything every time a snapshot is saved. Experimental, new in 16.3.

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:

Table 5. Compile step of next build, Bookshelf scratch app
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, turbopackFileSystemCacheForBuild: false

1.5 s

1.7 s

webpack (--webpack)

3.9 s

1.2 s (webpack has its own cache in .next/cache)

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.

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.

Table 6. Selected Turbopack flags under experimental (defaults for next build)
Option Default Effect

turbopackMinify

true

Minification of the output (off in dev).

turbopackRemoveUnusedExports, turbopackRemoveUnusedImports

true

Tree shaking of unused exports and imports (off in dev; the imports flag requires the exports flag).

turbopackScopeHoisting

true

Scope hoisting; always off in dev.

turbopackModuleIds

'deterministic' (build), 'named' (dev)

The module id strategy.

turbopackSourceMaps

follows productionBrowserSourceMaps (build), true (dev)

Source map generation.

turbopackChunking

see below

Size thresholds of the production chunker: minChunkSize 50000, maxChunkCountPerGroup 40, maxMergeChunkSize 200000, plus heuristics (firstPageLoadPriority, priorityRoutes, requestCost) and experimental component chunks. Fewer, larger chunks speed up the first load; more, smaller chunks are reused more across navigations.

turbopackLocalPostcssConfig

false

Resolve postcss.config.js from the CSS file’s folder first.

turbopackRustReactCompiler

not set

Run the native Rust port of the React Compiler inside Turbopack instead of the Babel transform. Needs reactCompiler: true and Turbopack.

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.

Table 7. Differences between Turbopack and webpack in Next.js 16.3
Area Turbopack behaviour

webpack config function

Not applied, and a plain next build refuses to run if one exists (see above). Port it to turbopack.rules and turbopack.resolveAlias, or use --webpack.

webpack plugins

Not supported at all. Loaders work (the subset above); plugins that hook the webpack compiler do not. @next/bundle-analyzer is one of them.

Filesystem root

Files outside the project root are not resolved, which breaks npm link and pnpm link of packages that live elsewhere. Set turbopack.root to a parent of both.

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 @import.

Sass

No ~ prefix for node_modules imports (use resolveAlias); no sassOptions.functions (those are JavaScript functions, which a Rust bundler cannot call).

CSS precision

Lightning CSS keeps 5 decimal digits, webpack 10: 25/17 becomes 1.47059, not 1.4705882353. Can show as sub-pixel differences.

Legacy CSS Modules features

No standalone :local and :global (only :global(…​)), no @value, no :import or :export ICSS rules, and composes or @import of a plain .css file treats it as global.

Unsupported options

Yarn Plug’n’Play, experimental.urlImports and experimental.esmExternals are not planned; experimental.nextScriptWorkers and experimental.fallbackNodePolyfills are planned.

Babel

Both bundlers can run it, but differently (see Babel, when you really need it).

Vite-style APIs

import.meta.glob and import.meta.env exist only with Turbopack: code that uses them does not build with --webpack.

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.json settings the compiler reads include jsxImportSource and experimentalDecorators (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 swcMinify flag was removed.

  • Can I strip or replace things at build time? Yes, through the compiler object, 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/jest sets up the SWC transform, mocks CSS and image imports, loads .env files and ignores node_modules and .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.

next.config.ts, compiler transforms
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;
app/api/compiler/route.ts
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
  });
}
app/[locale]/swc-demo/page.tsx
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.

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-loader for the files in your project (not node_modules, unless you configure the loader yourself). The build prints Using external babel configuration from <project>/.babelrc.

  • webpack disables SWC for the project: Disabled SWC as replacement for Babel because of custom Babel configuration, followed by compiler options in next.config.js will be ignored while using Babel (the pages swc-disabled and ignored-compiler-options under 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:

A custom Babel config that works under Turbopack
{
  "presets": ["next/babel"],
  "plugins": ["./babel-plugin-uppercase-marker.js"]
}
babel-plugin-uppercase-marker.js, a tiny plugin
// 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.

node_modules/@acme/ui/index.ts
export interface Price { cents: number; currency: "EUR" | "USD" }
export function formatPrice(p: Price): string {
  return `${(p.cents / 100).toFixed(2)} ${p.currency}`;
}
app/api/price/route.ts
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:

next.config.ts
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.

Table 8. When transpilePackages is needed, and when it is not
Situation What to do

A dependency in node_modules ships raw TypeScript or JSX

Add it to transpilePackages, or publish it compiled to JavaScript and point main and exports at the output.

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 apps/web importing packages/ui)

Add it to transpilePackages.

Pages Router, and you want a node_modules server dependency bundled into the route

The Pages Router loads such dependencies through Node.js require at runtime; list the package to bundle its source. The App Router already bundles Server Component and Route Handler dependencies.

The package must stay external on the server

Use serverExternalPackages instead. A package in both lists makes Next.js throw at the start of the build.

A large library with many modules (icon sets and similar)

Consider experimental.optimizePackageImports (it loads only the modules you import, and many popular libraries are optimised by default). Packages listed there, and the entries of Next.js’s internal default list, are transpiled automatically.

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.

next.config.ts, with the plugin
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
Output of 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 with withRspack exits with Cannot call withRspack and pass the --webpack flag. Please configure only one bundler.

  • a plain next start of the Rspack build exited at start with the same text. Reading the plugin’s index.js, it only accepts being loaded when the CLI has set TURBOPACK=auto (which next build and next dev do and next start does not) or when NEXT_RSPACK is already set. Starting the server with NEXT_RSPACK=true next start worked (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.

Pitfalls and Errors

Table 9. Common problems and what to do
Symptom Cause and fix

ERROR: This build is using Turbopack, with a webpack config and no turbopack config

A webpack function exists, often added by a plugin. Port it, add turbopack: {} after checking the app builds, or use --webpack.

Module not found: Can’t resolve 'fs' in code that reaches the browser

Refactor the import out of the client path; as a stop-gap alias fs to an empty module for the browser condition.

Unknown module type on a file in node_modules

The dependency ships TypeScript or JSX: add it to transpilePackages.

A console.log is missing in a production build

compiler.removeConsole is on. Use exclude for the levels you keep.

Compiled output ignores compiler.* options

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 @import.

A linked package outside the repository is not found

Turbopack does not resolve outside its root. Set turbopack.root to the common parent.

import.meta.glob returns an empty object

The pattern climbs out of the calling file. Use base, and assert the result is not empty.

Builds are not faster in CI

.next/cache is not preserved between builds, or you disabled turbopackFileSystemCacheForBuild.

Panic: Unable to resolve turbopackLoader

An import-attribute loader path is not relative to the importing file. Use a path relative to it, or a package name.

A .babelrc that worked on webpack makes the Turbopack build fail with a syntax error

The Babel config lacks next/babel, so Babel cannot parse TypeScript. Add the preset, or delete the file if SWC does everything you need (Next.js prints "a custom Babel configuration that can be removed").

What Changed Recently

Table 10. Tooling changes by Next.js version
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. transpilePackages is added (stable as "module transpilation" in 13.1).

13.5

optimizePackageImports supersedes modularizeImports.

15.0

Turbopack for next dev is stable. The swcMinify flag is removed from the config (the minifier is not customisable).

15.3-15.5

Turbopack for next build experimental in 15.3 and beta in 15.5. The top-level turbopack config replaces experimental.turbo (15.3; the old name still works as an alias).

16.0 (2025-10-21)

Turbopack is the default for next dev and next build. Automatic Babel when a config file exists. The top-level turbopack config leaves experimental. A webpack config with no turbopack config fails next build. --webpack is the opt-out. The file-system cache is beta (separate dev and build flags).

16.1 (2025-12-18)

The file-system cache for dev is on by default. next experimental-analyze (Turbopack bundle analyzer, still experimental).

16.2 (2026-03-18)

turbopackLoader import attributes, turbopack.rules.*.type and condition.contentType.

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; import.meta.glob (the Turbopack reference also documents import.meta.env); a native Rust React Compiler for Turbopack (experimental).

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-typescript with its caveats (no const enum, no export =), and shows customising it with a .babelrc that contains the next/babel preset plus plugins. Since 12 the default compiler is SWC and TypeScript is compiled by it, so those Babel-specific caveats no longer apply; a .babelrc still works but is a deliberate opt-in, and with webpack it disables SWC and the compiler options (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 webpack function in next.config.js for CSS preprocessors. In 16 Turbopack is the default; a webpack function fails a plain next 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 turbopack config, 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, and typeof window checks or use client boundaries 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). A webpack function is not applied and fails a plain next build.

  • Keep the file-system cache on and make .next/cache persistent in CI; memory eviction is automatic in dev.

  • import.meta.glob and import.meta.env are Turbopack-only.

  • The compiler options replace Babel plugins; a Babel config file changes how both bundlers behave.

  • transpilePackages is for dependencies that ship uncompiled code. Rspack is a community, experimental alternative.