Next.js Made Turbopack the Default: Here's What Actually Breaks
Next.js 16.3 switched next dev to Turbopack by default. Most teams won't notice until something breaks. Here's what changes, what silently fails, and the exact config fixes to sort it out.

The Change You Probably Missed
Next.js 16.3 made Turbopack the default bundler for next dev. No flag, no opt-in. Pull the latest version, run next dev, and you're on Turbopack.
For most projects this is invisible on day one. Then someone changes a webpack alias, a loader stops transforming files without warning, and nobody figures out why the dev build looks different from production.
This post covers what changed, what breaks, and what to do about each.
What Turbopack Does Differently
The core difference is lazy compilation. webpack processes your entire module graph at startup — finds every import, traces every dependency, bundles everything before the dev server responds to a single request. Turbopack compiles on demand, per route, as the browser requests pages.
Cold start time stops depending on project size. A 200-module app and a 2,000-module app start in roughly the same time because Turbopack hasn't compiled either of them yet. The tradeoff: navigating to a route you haven't visited yet takes 200-400ms while Turbopack compiles it. webpack pre-compiled everything, so route navigation felt instant. Most developers won't notice the hiccup. If you tab through lots of new routes while debugging, you will.
HMR is also faster because Turbopack tracks exactly which modules depend on a changed file. On a large component tree, hot reload drops from roughly 300ms to 80ms.
Three Things That Silently Break
1. Custom webpack config aliases
If your next.config.js has a webpack function, Turbopack ignores it during dev. It still runs for next build. An alias that works in production silently fails in development:
// next.config.js — runs for builds, not dev
module.exports = {
webpack(config) {
config.resolve.alias['@'] = path.resolve(__dirname, 'src')
return config
}
}Imports using @/components/Button resolve in production and break in development. Fix it by maintaining both configs:
module.exports = {
turbopack: {
resolveAlias: {
'@': './src',
},
},
webpack(config) {
config.resolve.alias['@'] = path.resolve(__dirname, 'src')
return config
},
}Yes, that's duplication. Yes, it's annoying. This goes away when Turbopack handles production builds, which isn't stable yet.
2. webpack loaders
Turbopack has its own loader API and doesn't run webpack loaders natively. Most common ones have equivalents by now, but niche plugins don't. The failure mode is silent: the loader skips, transformations don't happen, the file gets served as-is.
// webpack config — silently ignored in Turbopack dev
config.module.rules.push({
test: /\.svg$/,
use: ['@svgr/webpack'],
})
// Turbopack equivalent in next.config.js
turbopack: {
rules: {
'*.svg': {
loaders: ['@svgr/webpack'],
as: '*.js',
},
},
},Before assuming a loader works, check the Turbopack compatibility matrix. Anything from the webpack ecosystem needs verification.
3. CSS module composition edge cases
Turbopack enforces CSS module composes strictly. webpack was permissive about patterns that fall outside spec: composing from non-module files, or relying on load order when composing from global. If you see visual bugs in dev that don't show up in production, start here.
/* May break in Turbopack if source.css is not a CSS module */
.button {
composes: base from './source.css';
}Opting Out
The escape hatch:
next dev --no-turbopackOr in package.json:
{
"scripts": {
"dev": "next dev --no-turbopack"
}
}Set a reminder to remove this in a few weeks. The webpack dev path gets less maintenance attention going forward.
Production Builds Are Still webpack
next build hasn't changed. Turbopack production builds are on the roadmap but aren't stable yet.
This limits the blast radius. Dev behavior changed, production output didn't. You can't accidentally ship a Turbopack-specific bug to users. The risk surface is hot reload, asset paths during development, and sourcemaps.
Module Lifecycle Under the Hood
Turbopack tracks each module through a state machine. Understanding it explains both the speed gains and why certain cache states behave unexpectedly after errors:
When a file changes, Turbopack marks dependent modules Stale and recompiles only those. webpack's HMR did the same conceptually, but Turbopack's dependency graph is more granular, so fewer modules go stale per change.
The Failed state also surfaces faster than in webpack because Turbopack only compiled that route's modules before hitting the error. webpack would error after processing more of the graph.
The Actual Speed Numbers
On a mid-sized project (~500 modules), next dev cold start to first byte is roughly 3x faster on Turbopack. HMR latency on a complex component tree drops from ~300ms to ~80ms.
Those numbers hold at 1,000 or 5,000 modules too because the lazy compilation model doesn't scale with module count. webpack's startup time did.
The first visit to each route in dev carries a 200-400ms compile delay. webpack users won't notice this tradeoff until they're actively clicking through routes they haven't opened yet in that dev session.
What to Do Right Now
Run next dev and check two things: broken path aliases and visual regressions from loaders that silently stopped running. Both have the same signature — works in next build, fails in next dev.
If the migration is too disruptive right now, use --no-turbopack and block time to migrate the webpack config. Don't leave it there permanently.
The faster dev loop is worth the config work. On a large codebase where webpack startup was already grinding, the cold start improvement alone justifies the one-time migration effort.