Turbopack in Next.js: Incremental Computation, What It Supports and How It Compares to Webpack
Key takeaways
Turbopack is Vercel's Rust-based bundler for Next.js, designed to recompute only what changed. The post explains how that caching works, what to expect on small and large apps, its current limitations, and the checks to run before switching from a custom webpack config.
Introduction
Turbopack is an incremental bundler for JavaScript and TypeScript, written in Rust and developed by Vercel. The project is led by Tobias Koppers, who created webpack, and it is intended as the successor to webpack inside Next.js. It was first announced as an alpha alongside Next.js 13 in October 2022.
Why it can be fast
Speed claims are the least useful part of any bundler announcement, so it helps to understand where the time savings come from instead:
- Native code. Parsing and transforming run in Rust (Turbopack uses SWC for JavaScript and TypeScript), without JavaScript JIT warm-up or garbage-collection pauses.
- Incremental computation. Turbopack is built on a caching engine (Turbo Engine) that records the result of each small unit of work, such as parsing one module or resolving one import, along with what it depended on. After an edit, only the units whose inputs changed are recomputed.
- Lazy compilation. In development, a route is compiled only when you request it, so a large app with hundreds of pages does not pay for all of them at startup.
The consequence is that the benefit grows with app size. On a small app, webpack’s dev server is already quick and the difference may be hard to notice; on a large app with many routes, startup and update times are where teams tend to see the change.
Benchmarks and caution
Vercel’s launch-era benchmark claims, particularly the comparison with Vite, were publicly questioned by the Vite team, and the numbers depend a lot on how the test app was set up. I prefer to treat vendor benchmarks as a reason to try a tool, not as a number to promise. The practical test is simple: run next dev with and without Turbopack on your own app, open the heaviest route, edit a deeply imported component, and time both.
Where Turbopack stands
- Development:
next dev --turbopackis stable since Next.js 15. - Production builds:
next build --turbopackwas released as alpha and then beta during Next.js 15.x. Newer Next.js versions push Turbopack further as the default, so read the release notes for your version. - Outside Next.js: not supported as a standalone bundler.
- webpack plugins: not supported. A subset of webpack loaders can be run through Turbopack’s
rulesconfiguration.
Rspack, a Rust bundler from ByteDance, takes a different approach: it aims for compatibility with the webpack plugin API, which makes it an option for projects outside Next.js that cannot give up their webpack plugins.
Getting Started
Turbopack ships inside Next.js; there is nothing extra to install:
npx create-next-app@latest my-app
cd my-app
Enable Turbopack
# Development with Turbopack
npm run dev -- --turbopack
# Or update package.json
{
"scripts": {
"dev": "next dev --turbopack"
}
}
Older Next.js versions used the shorter --turbo flag. Recent versions of create-next-app ask whether to use Turbopack and write the flag into package.json for you.
Features
Automatic Configuration
TypeScript, JSX/TSX, CSS, CSS Modules, Sass (once the sass package is installed), PostCSS, static images, fonts through next/font, and JSON imports work without extra configuration. What you lose is anything that lived in a custom webpack() function, which Turbopack does not read.
Fast Refresh
// app/page.tsx
export default function Page() {
return <div>Hello, Turbopack!</div>;
}
// Edit and save: only the changed module and what depends on it are recompiled
Code Splitting
Automatic code splitting:
// Automatically creates separate chunk
const Heavy = dynamic(() => import('./HeavyComponent'));
CSS Support
Plain CSS
/* styles.css */
.button {
background: blue;
}
import './styles.css';
export default function Button() {
return <button className="button">Click</button>;
}
CSS Modules
/* Button.module.css */
.button {
background: blue;
}
import styles from './Button.module.css';
export default function Button() {
return <button className={styles.button}>Click</button>;
}
Tailwind CSS
// tailwind.config.js
module.exports = {
content: [
'./app/**/*.{js,ts,jsx,tsx}',
'./components/**/*.{js,ts,jsx,tsx}'
]
};
Tailwind runs through PostCSS, which Turbopack supports, so no Turbopack-specific setup is needed.
SCSS/SASS
npm install sass
// styles.scss
$primary: blue;
.button {
background: $primary;
}
import './styles.scss';
Image Optimization
Next.js Image optimization works with Turbopack:
import Image from 'next/image';
import logo from './logo.png';
export default function Page() {
return <Image src={logo} alt="Logo" width={200} height={200} />;
}
Environment Variables
# .env.local
NEXT_PUBLIC_API_URL=https://api.example.com
DATABASE_URL=postgresql://...
export default function Page() {
// Client-side (NEXT_PUBLIC_ prefix)
const apiUrl = process.env.NEXT_PUBLIC_API_URL;
return <div>{apiUrl}</div>;
}
Incremental Computation
The memoization happens inside the bundler, not in your components. Turbopack models its own work as many small functions: read this file, parse it, resolve its imports, transform it, put it in a chunk. Each result is cached together with the inputs it read. When you save ComponentA.tsx, the “read file” result for that path changes, which invalidates its parse and transform results and the chunk that contains it. The parse results for every other module are still valid and are reused.
// components/ComponentA.tsx: edited, so its parse/transform results are recomputed
export function ComponentA() {
return <div>Changed!</div>;
}
// components/ComponentB.tsx: untouched, so its cached results are reused
export function ComponentB() {
return <div>Unchanged</div>;
}
webpack also caches, but its unit of work is coarser and more of the compilation graph gets walked again after a change. That difference is small on a small app and grows as the module graph grows.
Measuring on Your Own App
Published comparisons use different apps, hardware, and versions, so the only numbers worth acting on are your own. A simple procedure:
- Delete
.nextand startnext devwithout Turbopack. Note the time until the first request to your heaviest route finishes. - Edit a component that many pages import and note how long the browser takes to show the change.
- Repeat both steps with
--turbopack. - Watch memory as well. Turbopack keeps a lot of cached state in memory, and on a large app that trade-off (more RAM for faster updates) matters on small laptops or constrained dev containers.
Next.js Integration
App Router
// app/page.tsx
export default function Home() {
return <h1>Hello, Turbopack!</h1>;
}
// app/layout.tsx
export default function RootLayout({ children }) {
return (
<html>
<body>{children}</body>
</html>
);
}
Server Components
// app/users/page.tsx (Server Component)
async function getUsers() {
const res = await fetch('https://api.example.com/users');
return res.json();
}
export default async function UsersPage() {
const users = await getUsers();
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
API Routes
// app/api/users/route.ts
export async function GET() {
const users = await db.user.findMany();
return Response.json({ users });
}
export async function POST(request: Request) {
const body = await request.json();
const user = await db.user.create({ data: body });
return Response.json({ user }, { status: 201 });
}
Debugging
When something behaves differently under Turbopack, the first question is whether the same code works with webpack. Run next dev without the flag; if the problem disappears, it is a bundler difference (usually a webpack customization that Turbopack ignores, or a package that relies on webpack-specific behavior) rather than a bug in your code. Next.js also documents a tracing mode for producing Turbopack performance traces; see the Turbopack page in the Next.js docs for the current environment variable, since it has changed between releases.
Practical Advice
Decide separately for dev and build
It is reasonable to use Turbopack for next dev while keeping webpack for next build until your version of Next.js marks Turbopack builds stable and you have compared the output:
{
"scripts": {
"dev": "next dev --turbopack",
"build": "next build"
}
}
The catch is that dev and production then use different bundlers, so a CSS ordering or module-resolution difference can show up only in production. Run the production build in CI on every pull request so those differences surface early.
Watch memory
Turbopack trades memory for speed by keeping its cache in memory. If the dev server is killed on a large app, look at the machine’s RAM before blaming the code.
Clear the cache when state looks stale
rm -rf .next
npm run dev -- --turbopack
Known Limitations
- webpack plugins are not supported. Anything implemented with a webpack plugin in
next.config.jsneeds a replacement or has to stay on webpack. - webpack loaders are partially supported through the
turbopack.rulesoption (loaders that only transform source code generally work; loaders that depend on webpack internals do not). - No standalone CLI. Turbopack is only supported through Next.js.
- Production builds depend on your Next.js version: alpha and beta during 15.x, and moving toward the default after that.
Migration from Webpack
Step 1: Update Next.js
npm install next@latest
Step 2: Enable Turbopack
{
"scripts": {
"dev": "next dev --turbopack"
}
}
Step 3: Test Your App
npm run dev
Apps without a custom webpack() function usually work without changes. Click through every route type you have (static pages, dynamic routes, route handlers, pages that import CSS from node_modules), because problems tend to show up in the less common paths.
Step 4: Move Custom Configuration
// next.config.js
module.exports = {
// Turbopack ignores the webpack() function; port what you can here
turbopack: {
resolveAlias: {
// e.g. replace a webpack alias
},
rules: {
// e.g. run a source-transforming loader such as @svgr/webpack on *.svg
},
},
};
In Next.js 15.2 and earlier this block lived under experimental.turbo; newer versions use the top-level turbopack key.
Frequently Asked Questions (FAQ)
Q. Why does my webpack config stop working when I switch to Turbopack?
A. Turbopack does not read the webpack() function in next.config.js, so custom loaders, plugins, and aliases defined there are ignored. Some common loaders can be mapped through Turbopack’s own rules and resolve-alias options, but webpack plugins have no direct equivalent. Before migrating, list what your webpack customization does and check each item against Turbopack’s documented support, keeping webpack for the builds that still depend on unsupported plugins.
Related Articles
- SWC: Rust-Based JavaScript Compiler
- esbuild
- Next.js 15 Internals: App Router vs Pages Router, RSC, Rendering Decisions and the Four Caches
- Webpack
- Vite