On Next.js 16, ship Turbopack as the default for next dev and next build. Keep Webpack when a concrete webpack plugin, custom Sass JS function, or unsupported toolchain still depends on the Node webpack runtime. Opt out with next build --webpack (and next dev --webpack if you must match production), not by leaving a dead webpack() block in next.config and hoping Turbopack ignores it.
The upgrade docs are explicit: Turbopack is stable and default; --turbopack / --turbo flags are no longer required in scripts; and if a custom webpack() configuration is present, next build fails on purpose so you do not ship a misconfigured production bundle.
What changed in Next.js 16 (bundler scope)
Before 16 you opted into Turbopack. In 16 you opt out of it. Official upgrade guidance says to drop --turbopack from package scripts so next dev and next build use Turbopack with no extra flags. Blog numbers from the Next.js 16 announcement put Fast Refresh gains in the "up to 5–10x" range and production builds "2–5x" in the apps they measured; treat those as directional, and measure your own repo with and without --webpack.
Filesystem caching of compiler artifacts is on by default for both development and production builds (experimental.turbopackFileSystemCacheForDev / ForBuild). That matters more than a one-shot cold build number: large monorepos feel the restart cost, not the first CI run. Disable the build cache when your CI discards .next/cache every job and you want to avoid writing cache you never reuse.
Config moved too. Prefer a top-level turbopack key in next.config over the old experimental.turbopack / experimental.turbo aliases. Loaders you still need go under turbopack.rules. Path aliases map through turbopack.resolveAlias (including the old webpack resolve.fallback pattern for Node builtins that leak into client code).
Decision matrix: stay on Turbopack or pin Webpack
I keep Turbopack when:
- The app is App Router + TypeScript + CSS Modules / Tailwind / PostCSS with no custom webpack plugins.
- Package imports are normal
node_modules(no Yarn PnP; nonpm linkof packages outside the Turbopack root withoutexperimental.turbopackAdditionalRoots). - Babel is present for a real transform, not as cargo cult (Next 16 Turbopack will auto-enable Babel when it finds a Babel config; SWC still handles Next's own transforms).
- Sass is plain
@use/@importwithoutsassOptions.functionsthat inject JavaScript callables.
I pin Webpack with --webpack when:
- A third-party tool still requires a webpack plugin. Turbopack documents that webpack plugins are not supported; webpack loaders are the supported escape hatch via
turbopack.rules. - You rely on custom Sass functions defined in JS (
sassOptions.functions). Turbopack's Rust pipeline cannot execute those callbacks. - You are on a platform without native Turbopack bindings and WASM fallback is not enough (docs point FreeBSD/OpenBSD-style hosts at
--webpack). - You need a temporary parity window: Turbopack in
dev, Webpack inbuild, while you migrate plugins one by one.
If a plugin injects webpack() into your config without you writing it, Next still fails the Turbopack build. That is the trap I see most on upgrades: the failure looks like "your" webpack config, but the author is an analytics, Storybook, or Sentry helper that still assumes webpack is the default bundler.
The upgrade path that does not lie to CI
- Remove
--turbopackfrom scripts. Runnext buildonce on a clean machine. - If the build fails because
webpack()exists, inventory who owns it. Prefer migrating that behavior toturbopack.rules/resolveAlias, or delete the plugin if it existed for a webpack-era polyfill. - If you must keep the plugin for a release, set
"build": "next build --webpack"and open a ticket with an expiry date. Do not leave--webpackunmarked in CI forever. - For linked packages outside the project root, configure
turbopack.root/experimental.turbopackAdditionalRootsinstead of assuming webpack's looser filesystem walk. - Drop legacy Sass
~imports (@import '~pkg/...'). Turbopack does not honor the tilde; map~*withresolveAliasif you cannot rewrite the imports yet. - Spot-check CSS Module order and Lightning CSS decimal precision. Turbopack follows JS import order more strictly and uses 5 digits of precision versus webpack's 10. Most apps never notice; design systems with fractional
line-heightsometimes do.
Example scripts when production still needs Webpack for one plugin:
1{2 "scripts": {3 "dev": "next dev",4 "build": "next build --webpack",5 "start": "next start"6 }7}
Example when you ignore a leftover webpack() block and force Turbopack:
1next build --turbopack
Use that second form while you delete the dead config. Shipping --turbopack forever while a webpack() function still sits in the file is how teams forget why builds diverge between laptop and CI.
Package bundling and client boundaries (related, not the same decision)
Turbopack vs Webpack is about how the toolchain compiles and chunks modules. optimizePackageImports and 'use client' boundaries are about how much JavaScript you ship after the bundler runs. Do not "fix" a heavy Client Component tree by flipping bundlers. If Emotion or another CSS-in-JS runtime still needs a webpack-era path (some Chakra App Router setups still document next build --webpack for hydration quirks), that is a plugin/runtime constraint, not a reason to blame Turbopack for your client bundle size.
For Node built-ins that leak into client graphs, prefer fixing the import graph. When you cannot, Turbopack's resolveAlias with a browser-empty module replaces the old webpack resolve.fallback silence pattern. Treat that as a temporary bandage; it does not make fs available in the browser.
What I recommend on client projects this month
Default every Next.js 16 greenfield app to Turbopack with empty webpack config and no PnP. On upgrades, spend the first hour hunting webpack plugins and Sass JS functions, not debating benchmark screenshots. Keep Webpack behind an explicit --webpack script with a written reason in the PR. Revisit that flag every release: the list of "we need this plugin" shrinks faster than most teams expect once loaders move under turbopack.rules.
If the blocker is a single analytics webpack plugin that injects a define or banner, replace it with a small turbopack.rules loader or a post-build step. If the blocker is a design-system Sass function library, either port those helpers to plain Sass / CSS variables or accept Webpack for that app until the design system drops the JS hook.