Next.js 16: Turbopack, React Compiler & Cache
Turbopack as the Default Bundler
What Changed in Next.js 15 and What Is Coming
Next.js 15 ships Turbopack for the development server via the --turbopack flag and is stabilizing it for production builds. Webpack remains the default production bundler. A future Next.js release will make Turbopack the default for both development and production builds. Webpack will stay available as a fallback, but Vercel is shifting primary development investment toward Turbopack.
New projects created with create-next-app in Next.js 15 prompt the user to opt into Turbopack; a future version will likely enable it by default.
Migration Checklist: Moving from Webpack to Turbopack
The transition from webpack to Turbopack requires systematic auditing. The following checklist covers the critical migration steps:
- Audit
next.config.jsfor custom webpack configurations. Anywebpack()function overrides will not carry over to Turbopack automatically. - Identify unsupported webpack plugins and replace
webpack()function overrides with Turbopack loader configuration. Not every webpack plugin has a direct counterpart in Turbopack's loader system. Turbopack uses arulesobject instead of webpack's configuration object passed by reference; translate each plugin and override individually. - Update
.babelrcor Babel configuration. Turbopack uses SWC for transpilation; Babel is not supported. Custom Babel plugins need SWC equivalents or must be removed. - Verify CSS module and PostCSS compatibility. Turbopack supports CSS modules and PostCSS, but edge cases in custom PostCSS plugin chains should be tested.
- Test third-party packages that hook into webpack internals for code generation or asset handling. These often break silently under Turbopack.
- Run
next buildand capture the full output for review. Pipe the output to a log file to surface compatibility issues that might not cause hard failures but affect output. (See upgrade sequence below for the exact command.) - Benchmark build times before and after migration. Document a baseline with webpack so improvements (or regressions) are measurable.
The most common migration task involves translating custom webpack loaders and aliases into Turbopack's configuration format. Here is a before-and-after comparison of a next.config.js that adds an SVG loader and path aliasing:
// next.config.js - BEFORE (Next.js 15 with webpack)
/** @type {import('next').NextConfig} */
const nextConfig = {
webpack(config) {
config.module.rules.push({
test: /\.svg$/,
use: ['@svgr/webpack'],
});
config.resolve.alias = {
...config.resolve.alias,
'@components': './src/components',
'@lib': './src/lib',
};
return config;
},
};
module.exports = nextConfig;
// next.config.js - AFTER (Turbopack configuration)
// In Next.js 15, this config lives under experimental.turbo.
// A future release may promote it to a top-level turbopack key.
// Verify the correct key for your installed version.
//
// NOTE: If your package.json includes "type": "module", __dirname is not
// available. In that case, use the ESM-safe form shown below this example.
const path = require('path');
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
turbo: {
rules: {
'*.svg': {
loaders: ['@svgr/webpack'],
as: '*.js',
},
},
resolveAlias: {
'@components': path.resolve(__dirname, 'src/components'),
'@lib': path.resolve(__dirname, 'src/lib'),
},
},
},
};
module.exports = nextConfig;
If your project uses ESM ("type": "module" in package.json), __dirname is not available. Use this form instead:
// next.config.js - AFTER (Turbopack configuration, ESM-safe)
import { fileURLToPath } from 'url';
import { dirname, resolve } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
turbo: {
rules: {
'*.svg': {
loaders: ['@svgr/webpack'],
as: '*.js',
},
},
resolveAlias: {
'@components': resolve(__dirname, 'src/components'),
'@lib': resolve(__dirname, 'src/lib'),
},
},
},
};
export default nextConfig;
Note: Verify that @svgr/webpack works under Turbopack's loader compatibility layer before relying on it. As of Next.js 15, some webpack loaders are unsupported. Consult the Turbopack documentation for the supported loader list. Use path.resolve(__dirname, '...') for reliable alias resolution rather than relative string paths. Loaders are declared under rules with glob patterns as keys, and the as property tells Turbopack how to treat the output. Path aliases move into resolveAlias as a flat object.
Falling Back to Webpack (When and How)
For projects with deep webpack plugin dependencies or monorepo tooling that has not yet been updated for Turbopack, continuing to use webpack is straightforward. In Next.js 15, webpack is the default production bundler. To use Turbopack for development only, add --turbopack to your next dev command. No config key is needed to restore webpack; simply omit the --turbopack flag.
Acceptable use cases for staying on webpack include legacy plugin dependencies that have no SWC or Turbopack equivalent, monorepo setups with custom webpack federation configurations, and projects using Babel plugins with no SWC alternative. Track Turbopack's compatibility progress via the Turbopack roadmap and issue tracker.
Performance Benchmarks at a Glance
The following table shows hypothetical figures for a mid-size Next.js project (~200 routes, ~150 dependencies). These are not cited benchmarks:
| Metric | Webpack | Turbopack |
|---|---|---|
| Cold production build | ~120s | ~45s |
| Incremental rebuild (production) | ~18s | ~4s |
| Dev server startup | ~8s | ~1.2s |
These numbers are illustrative, not measured against a specific public benchmark. For cited benchmarks with methodology, see the Turbopack benchmark page on turbo.build. Real-world results vary depending on project size, the number of dependencies, and the complexity of custom configurations. Teams should run their own benchmarks using the baseline documentation step from the migration checklist to validate improvements in their specific context.
The React Compiler: From Experimental Toward Default
What the React Compiler Does
The React Compiler performs automatic memoization of components and hooks at build time. It analyzes component purity and dependency graphs to determine which values can be safely memoized. When the compiler cannot prove purity, it leaves the component unoptimized. For components it can verify, it inserts the equivalent of useMemo, useCallback, and React.memo calls into the compiled output, eliminating unnecessary re-renders without requiring developers to manually wrap values and callbacks.
The compiler operates on the principle that React components should be pure functions of their props and state. When it verifies that a component or expression meets this contract, it applies granular memoization that would be tedious and error-prone to maintain by hand.
Enabling and Configuring the React Compiler
As of Next.js 15, the React Compiler is available as an experimental opt-in feature. A future release will enable it by default, based on the current RFC trajectory. The React Compiler requires React 19 (or React 17+ with the react-compiler-runtime shim). In Next.js 15, the documented configuration is:
// next.config.js - React Compiler configuration (Next.js 15)
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
reactCompiler: true,
},
};
module.exports = nextConfig;
Note: The exact config shape for excluding specific paths (e.g., an exclude array) should be verified against the React Compiler documentation and your installed Next.js version's config schema. The shape may differ from what is shown in community examples.
To disable the compiler, remove the reactCompiler key or set it to false. For individual components or functions that need to opt out, the "use no memo" directive can be placed at the top of a file or at the top of a specific function body:
// src/components/IdentitySensitive.jsx
"use no memo";
import { useEffect, useRef } from 'react';
// This component relies on referential identity of the callback
// for integration with an external charting library that performs
// its own equality checks on callback references.
export default function IdentitySensitive({ onDataPoint }) {
const chartRef = useRef(null);
useEffect(() => {
// External library stores and compares callback references internally
chartRef.current?.bindCallback(onDataPoint);
}, [onDataPoint]);
return <div ref={chartRef} />;
}
The "use no memo" directive tells the compiler to skip automatic memoization for the entire file (when placed at the file level) or for a specific function (when placed at the top of a function body). This is appropriate when a component depends on intentional referential identity behavior that the compiler's memoization would break.
Refactoring Existing Code: What to Remove, What to Keep
With the React Compiler active, manual useMemo, useCallback, and React.memo wrappers will often become redundant. Verify using the compiler's annotation output before removing them to avoid performance regressions. The compiler handles these optimizations automatically and often more granularly than hand-written memoization, but blanket removal without verification is not recommended.
Potentially safe to remove (after verification): useMemo wrapping derived values from props or state, useCallback around event handlers passed to child components, and React.memo on components that are already pure.
Keep: Memoization tied to expensive non-rendering computation, such as heavy data transformations inside event handlers or effects that are not part of the render path. The compiler optimizes rendering; it does not optimize arbitrary JavaScript computation.
Here is a before-and-after comparison:
// BEFORE - Manual memoization (Next.js 14/15)
import { useMemo, useCallback, memo } from 'react';
const ProductCard = memo(function ProductCard({ product, onAddToCart }) {
// Compiler handles this automatically - safe to remove useMemo
const discountedPrice = useMemo(
() => product.price * (1 - product.discount),
[product.price, product.discount]
);
// Compiler handles this automatically - safe to remove useCallback
const handleClick = useCallback(() => {
onAddToCart(product.id, discountedPrice);
}, [onAddToCart, product.id, discountedPrice]);
return (
<div>
<h3>{product.name}</h3>
<p>${discountedPrice.toFixed(2)}</p>
<button onClick={handleClick}>Add to Cart</button>
</div>
);
});
export default ProductCard;
// AFTER - React Compiler handles memoization
// The compiler automatically memoizes discountedPrice, handleClick,
// and the component itself based on dependency analysis.
// Verify compiler output confirms these optimizations before
// removing manual memoization in your codebase.
// NOTE: Without the React Compiler active, the inline arrow function
// creates a new reference on every render. Only remove useCallback
// after confirming the compiler is enabled and optimizing this component.
export default function ProductCard({ product, onAddToCart }) {
const discountedPrice = product.price * (1 - product.discount);
const handleClick = () => {
onAddToCart(product.id, discountedPrice);
};
return (
<div>
<h3>{product.name}</h3>
<p>${discountedPrice.toFixed(2)}</p>
<button onClick={handleClick}>Add to Cart</button>
</div>
);
}
The eslint-plugin-react-compiler package can be added to a project's ESLint configuration to flag violations of the Rules of React that would prevent the compiler from optimizing effectively.
The New Cache API
Why the Old Caching Model Is Being Replaced
Next.js 14 and early 15 releases aggressively cached fetch() results and route data by default. This led to widespread developer confusion and stale data bugs, particularly in dynamic applications where data freshness was critical. The implicit caching behavior meant that developers often could not predict whether a page was serving fresh or cached content without deep knowledge of the framework's internal caching layers.
The new direction shifts to an opt-in caching model where developers must explicitly declare caching behavior using semantic APIs.
The "use cache" Directive and cacheLife / cacheTag APIs
Prerequisite: These APIs are experimental in Next.js 15. To enable them, add experimental: { useCache: true } to your next.config.js.
Comments
No comments yet. Start the discussion.