The Rust Wave and the 2025–2026 Release Map: Rolldown, Oxc, Rspack, Turbopack, Lightning CSS and Choosing a Tool
In 2025–2026 build tools written in JavaScript were replaced by ones written in Rust and Go: Vite 8 moved to Rolldown and Oxc, the Angular CLI to Rolldown for chunk optimization, Next.js 16 to Turbopack, and Rspack 2.0 offered a webpack-compatible path. Measuring the Bookstore widget with four tools, we see Rspack ahead on the small project, Rolldown and esbuild ahead at 2000 modules, most of webpack's time spent in Terser, and we end with a decision table.
Version note: The examples in this post were run with Vite 8.3 (Rolldown 1.2), Rolldown 1.3, esbuild 0.28, Rspack 2.3, webpack 5.112, Angular CLI 22.2, TypeScript 7.0 and Node.js 24; browser measurements were taken with Playwright 1.65's Chromium. Timings come from the author's own machine; look at the ratios between tools, not the absolute values.
Guess First
In this series we built the "book finder" widget of Bookstore, the online bookstore used as the shared example across this site's series, with four tools: Vite, esbuild, webpack and Rspack, which we add in this post. According to the announcements Rspack is many times faster than webpack, and Rolldown is the newest bundler written in Rust. We run the widget's production build with each tool's own CLI, five times on the same machine, and compare the medians. What order do you expect? How many times faster is the fastest tool than the slowest? Does the order change when the same four tools run on a project with 2000 modules?
This is the last post of the Build Tools series. We gather what happened in build tools from mid-2025 to late 2026 into a release map: how Vite changed with Rolldown and Oxc, Rspack and Rsbuild 2.0, where webpack and esbuild stand, Lightning CSS and Turbopack. Then we talk about how to repeat the measurements used in this series in your own project and which tool to choose in which situation.
The 2025–2026 Release Map
We took the dates from npm records and the projects' own announcements:
| Date | Release | What changed? |
|---|---|---|
| June 10, 2025 | oxlint 1.0 | Oxc's linter reached a stable release |
| June 24, 2025 | Vite 7.0 | Node.js 18 support ended (20.19+ and 22.12+), the default build.target became baseline-widely-available |
| October 21, 2025 | Next.js 16 | Turbopack became the default bundler for both dev and production builds |
| November 9, 2025 | esbuild 0.27.0 | The 0.27 series started |
| February 4, 2026 | webpack 2026 roadmap | The road to webpack 6, and native CSS, HTML and TypeScript in 5.10x |
| March 12, 2026 | Vite 8.0 | Rolldown replaced esbuild and Rollup in production builds and pre-bundling, Oxc does transforms and JS minification, Lightning CSS does CSS minification |
| April 2, 2026 | esbuild 0.28.0 | with { type: 'text' } imports, an extra integrity check on install, Go 1.26 |
| April 22, 2026 | Rspack and Rsbuild 2.0 | ESM-only packages, better tree shaking, tree shaking for shared dependencies in Module Federation |
| May 7, 2026 | Rolldown 1.0 | The public API is now bound to semantic versioning |
| June 3, 2026 | Angular 22.0 | webpack builders deprecated, chunk optimization on by default |
| July 29, 2026 | Angular 22.1 | Rolldown does the chunk optimization, builds write debug IDs |
The common theme of the table is clear: tools written in JavaScript (Rollup, Terser, Babel, PostCSS-based chains) are being replaced by tools written in Rust and Go. But most of these tools are not used on their own; they go inside higher-level tools such as Vite, the Angular CLI and Next.js. As an application developer you do not call Rolldown directly; you upgrade to Vite 8 or Angular 22.1 and Rolldown comes along.
Rolldown and Oxc: Vite's New Engine
Rolldown is a bundler written in Rust that is compatible with Rollup's plugin API. We saw why Vite needed it in the Vite post of this series: up to Vite 7 there was esbuild on the dev side and Rollup in production, and the two tools could process the same code with different behavior. Vite 8 replaced both with Rolldown. With Rolldown 1.0 released in May 2026, its API became stable too; at the time of writing the latest release is 1.3.2, while Vite 8.3.8 uses 1.2.19.
Oxc (JavaScript Oxidation Compiler) is the same team's toolset of parser, transformer, resolver, minifier and linter. Rolldown hands TypeScript and JSX transformation to Oxc, and Vite 8's JavaScript minifier is Oxc too. The Angular CLI is heading the same way: as we saw in the Angular CLI post of this series, the advanced optimization transforms moved from Babel to oxc-parser in 22.1, and the Angular linker in 22.2. Oxc's linter, oxlint, aims to run most ESLint rules much faster; we cover that side in the Static Analysis series.
Lightning CSS is the counterpart on the CSS side: a CSS parser, transformer and minifier written in Rust. In Vite 8 it does CSS minification by default; when we saw CSS nesting flattened for the target browsers in the first post of the series, it was the tool working behind the scenes.
Rspack and Rsbuild 2.0: webpack-Compatible Rust
Rspack is a bundler written in Rust that keeps webpack's config format and most of its loader and plugin API. If you have a large webpack legacy, moving to Vite means rewriting the whole build; moving to Rspack usually means adapting the config. To try this, we ported the widget's webpack config to Rspack:
import path from "node:path";
import { defineConfig } from "@rspack/cli";
import { rspack } from "@rspack/core";
// The same widget as webpack.config.mjs, with Rspack's built-in SWC loader, CSS and HTML plugin
export default defineConfig((env, argv) => {
// Content hashes only in production: in dev the CSS hot update looks for the old file name
const hash = argv.mode === "development" ? "" : ".[contenthash:8]";
return {
entry: { index: "./src/main.ts" },
// Without targets SWC and the runtime fall back to old syntax (async/await becomes generator helpers)
target: ["web", "es2022"],
output: {
path: path.resolve(import.meta.dirname, "dist-rspack"),
filename: `[name]${hash}.js`,
cssFilename: `[name]${hash}.css`,
clean: true,
},
resolve: { extensions: [".ts", ".js"] },
module: {
rules: [
{ test: /\.ts$/, loader: "builtin:swc-loader", options: { jsc: { parser: { syntax: "typescript" } }, env: { targets: ["chrome 111", "edge 111", "firefox 114", "safari 16.4"] } }, type: "javascript/auto" },
{ test: /\.css$/, type: "css/auto" },
],
},
plugins: [
// index.html loads /src/main.ts for Vite, so Rspack gets its own minimal page
new rspack.HtmlRspackPlugin({
title: "Bookstore book finder",
templateContent: '<!doctype html><html lang="en"><head><meta charset="utf-8"><title>Bookstore book finder</title></head><body><div id="book-finder"></div></body></html>',
}),
],
};
});
The differences are small: for TypeScript, Rspack's built-in SWC-based builtin:swc-loader instead of ts-loader; for CSS, the css/auto module type instead of a separate loader; for HTML, the built-in HtmlRspackPlugin instead of html-webpack-plugin. We did not use the HTML entry from our webpack config (a new webpack 5.10x feature) in Rspack; the plugin produces the page. We also hit a bug while porting. The first config always had [contenthash:8] in the file names; when we changed the CSS under rspack serve, these lines appeared in the browser console and the style did not update (port written as Rspack's default 8080):
Refused to apply style from 'http://localhost:8080/index.c1db4833.css?hmr=...' because its MIME type ('text/html') is not a supported stylesheet MIME type, and strict MIME checking is enabled.
[HMR] Update failed: Loading css hot update chunk index failed.
The hot update asks for the CSS by its old hashed name, the dev server cannot find that name and returns HTML. Once we kept the hash for production only, the CSS change was applied without a page reload. The second surprise was the output size: the JavaScript file of the first Rspack build was 8.26 kB, webpack's 3.77 kB. Opening the file showed why: async/await had been turned into generator-based helper functions. When builtin:swc-loader and Rspack's target get no targets, the code is transformed to old syntax. Giving it the browsers of Vite's Baseline target brought the file down to 3.44 kB; in webpack's output async/await was left as it was anyway. Note also that Rspack 2.0 publishes its own packages as ESM only; that is why writing the config as .mjs causes no trouble.
Rsbuild is a build tool built on top of Rspack that, much like Vite, comes with ready-made defaults; it moved to 2.0 on the same day as Rspack 2.0. In a new project, starting with Rsbuild instead of configuring Rspack directly is the shorter path for those who want the flexibility of a webpack-style config without dealing with the details.
webpack, esbuild and Turbopack: Where Do the Others Stand?
webpack is not dead. Between 5.107 and 5.112, native CSS, HTML entries and built-in TypeScript support entered the core; the 2026 roadmap describes webpack 6 as a release that works with less configuration. We saw these in detail in the webpack post of this series. But Angular 22.0 deprecated its webpack builders and Next.js 16 switched its default to Turbopack; webpack's two biggest users are moving away from it. One strong reason remains to choose webpack for a new project: a need whose ecosystem is built around webpack, such as Module Federation. Even then, Rspack offers the same API faster.
esbuild still runs inside many tools: it is the tool that bundles application code in the Angular CLI. 0.28.0 (April 2026) brought importing text files with with { type: 'text' } and an integrity check on the installer's fallback download path; the Go version it is compiled with became 1.26. The limits from the esbuild post of this series (no HMR, code splitting only for ESM) have not changed; esbuild is still the simplest option for small, single-purpose jobs.
Turbopack is Next.js's own bundler. With Next.js 16 both next dev and next build use it by default; you can go back with next build --webpack. Turbopack is developed as part of Next.js, not as a standalone tool; it is not something you can pick for an Angular or Vite project. If you do not use Next.js, in this release map it is only the sign of a trend: frameworks choose the bundler themselves.
Your Own Measurement: Production Build
The performance numbers in announcements come from projects those teams picked. In the Vite post of this series we quoted the Vite team's Linear, Ramp and Beehiiv numbers and left our own measurement for this post. First, the measurement from the guessing question: the widget's production build, with each tool's own CLI, one warm-up and five measured runs:
// Runs each tool's production build through its own CLI (1 warm-up + 5 runs) and prints the median,
// the ratio to the fastest tool and the gzip size of the emitted JS and CSS.
const tools = {
"vite (Rolldown)": { args: ["node_modules/vite/bin/vite.js", "build"], out: "dist" },
"esbuild": { args: ["esbuild.config.mjs"], out: "dist-esbuild" },
"rspack": { args: ["node_modules/@rspack/cli/bin/rspack.js", "build"], out: "dist-rspack" },
"webpack": { args: ["node_modules/webpack-cli/bin/cli.js", "--mode", "production"], out: "dist-webpack" },
};
npm run bench:build
vite (Rolldown) 2.2x 2.17 KiB gzip (JS + CSS)
esbuild 1.2x 2.26 KiB gzip (JS + CSS)
rspack 1.0x 1.91 KiB gzip (JS + CSS)
webpack 5.3x 1.97 KiB gzip (JS + CSS)
The order is not what most people guess: in this small project Rspack is the fastest, and Vite is about twice as slow. The reason lies in what we measured. The widget has about twenty modules; the build itself takes milliseconds, and most of the total time is starting the Node.js process and loading the CLI and the config. On every start Vite first compiles vite.config.ts and then loads it, while Rspack's CLI hands over to its native binding quickly. Part of webpack's roughly 5 times gap also comes from loading a tool chain written in JavaScript. The output sizes are close; Vite's is a little larger because it includes the modulepreload polyfill.
As the project grows, the picture changes. We added Rspack to the measurement script from the esbuild post of this series and extended the script to generate N small modules that import each other. Here the tools run in the same Node.js process, through their JS APIs; process startup is not part of the measurement:
npm run bench:scale -- 1000
npm run bench:scale -- 2000
1000 modules
esbuild 1.2x
rolldown 1.0x
rspack 3.6x
webpack 71.9x
2000 modules
esbuild 1.3x
rolldown 1.0x
rspack 5.6x
webpack 137.6x
Rolldown and esbuild are close, and their ratio does not change as the module count grows. Rspack is about 3.6 times slower at 1000 modules and 5.6 times at 2000; webpack is between 70 and 140 times slower, and that ratio moves from run to run (in an earlier run of the same 2000-module measurement it was 196 times). We looked at where webpack's time goes: in the 2000-module build, turning minification off cut the time to about one twenty-eighth. Most of the gap comes from Terser, webpack's default minifier, which is written in JavaScript; minifying code that scope hoisting gathered into one huge scope is heavy work for Terser. That is why in webpack projects the first and cheapest improvement is replacing the minifier with one based on esbuild or SWC; changing the bundler is the second step.
A warning: the generated modules do not look like a real application; they all follow the same template, with no CSS or assets. This measurement shows how the tools scale, not how long your project will take.
Your Own Measurement: Dev Server and HMR
What a developer waits for most during the day is not the production build, but the dev server starting and a saved change reaching the screen. We measured that with Playwright too (playwright was added to the widget as a devDependency; run npx playwright install chromium once before the first run). The script starts each dev server on a free port, records the time until the widget appears in Chromium, then changes a color in styles.css and the placeholder text in finder.ts and measures the time until each change shows up on the page:
async function timeUntil(page, edit, check, expected) {
const start = performance.now();
edit();
for (;;) {
try {
// A full page reload destroys the context while waiting; then we simply wait again
await page.waitForFunction(check, expected, { timeout: 30_000, polling: 10 });
return performance.now() - start;
} catch (error) {
if (performance.now() - start > 30_000) {
throw error;
}
}
}
}
npm run bench:dev
tool first render CSS change TS change
vite 1.4x 1.0x 1.5x
rspack 1.0x 1.4x 1.0x
webpack 3.0x 2.3x 1.1x
On first start Rspack leads; Vite takes about 1.4 times as long and webpack 3 times. For the CSS change the order flips: Vite is the fastest, Rspack 1.4 and webpack 2.3 times. Here the absolute value matters as well: on this machine the CSS change reached the screen in under 100 ms with all three tools; a user will not feel the difference. For the TypeScript change all three tools reload the whole page, because, as we saw in the Vite post of this series, the widget has no HMR boundary. Here Vite is 1.5 times slower; a likely reason is that on the reloaded page Vite serves every module with a separate request, while the bundling tools hand over a single file. The variation between measurements is large too: in earlier runs of the same script Rspack's CSS ratio ranged from 1.4 to 4.7. In a project with twenty modules these differences are not big enough to decide the choice of tool; in a large project you need to repeat the measurement with your own code.
Decision Table
We gather what we saw in this series into a decision table:
| Situation | Recommended tool | Why |
|---|---|---|
| New SPA or library | Vite | Fast unbundled startup in dev, Rolldown in production, a large plugin ecosystem |
| Angular application | The Angular CLI's @angular/build:application builder | It manages esbuild, Vite and Rolldown together with the Angular compiler; no separate bundler to choose |
| Large webpack legacy | Rspack (or Rsbuild) | Most of the config and plugins carry over, and build time drops noticeably |
| Micro-frontends with Module Federation | Rspack or webpack | Module Federation's ecosystem is built around the webpack API |
| A small script in an MPA or a Razor page | esbuild or a Vite library build | One command, few dependencies; if no dev server is needed, esbuild is enough |
| Next.js application | Turbopack | The framework's default; not a separate choice |
The MPA row is Bookstore.Web's situation: we chose Vite for the widget embedded in the Razor page because we wanted HMR and manifest integration; if we had only wanted to minify a single script, esbuild would have been enough.
Production Notes
Repeat the measurement in CI; do not make it a one-off. A bundler update can change build time or output size; comparing the output of bench:build across releases makes that visible. Watch ratios and trends on the same machine, not absolute times.
Validate a tool change on the output first. As we saw in this series, behavior can change when the tool changes: moment's locale files went into the bundle with webpack and not with esbuild; esbuild.drop was silently ignored when moving to Vite 8. Compare the list of output files, their sizes and how the application behaves in the browser with the old build.
Watch out for native bindings. Rolldown, Rspack, Oxc and Lightning CSS ship as platform-specific binary packages (win32-x64 on Windows). If npm ci runs on a different operating system in CI, or installs with --omit=optional, these packages can be missing; the error usually reads like "cannot find binding".
Common Pitfalls
First, carrying the performance number from an announcement over to your project. The numbers come from projects those teams picked; in a small project process startup can dominate, in a large project the minifier or the type check.
Second, measuring the wrong thing. Measuring through the CLI includes process startup, measuring through the JS API does not; they answer different questions. Decide first which time you want to shorten: the production build in CI, or the HMR a developer waits for on every save?
Third, thinking "it is written in Rust, so it is fast at everything". In our measurement Rspack was the fastest on the small project and behind Rolldown and esbuild on the large one; and most of webpack's slowness came not from the bundler but from the minifier.
Fourth, interfering with a build the framework manages. Angular and Next.js choose the bundler themselves; adding a separate Vite or Rspack config to an Angular project means giving up the guarantees the CLI provides.
Fifth, carrying the old config over as is when switching tools. As we saw with the contenthash bug in Rspack, a setting that works fine in one tool can break HMR in another.
A Small Experiment
★☆☆ Run npm run bench:build three times in snapshot/08/widget. How much do the ratios change from run to run? Then turn off optimization.minimize in the webpack config and repeat the measurement.
★★☆ Add Vite itself (through the build function in its JS API) to bench/scale.mjs as a fifth tool. How much does Vite add on top of Rolldown at 1000 and 2000 modules?
★★★ Set up a similar measurement for Bookstore's Angular app: compare the cold (.angular/cache deleted) and warm times of ng build, with NG_BUILD_OPTIMIZE_CHUNKS on and off. Confirm how much of the total time chunk optimization takes with the NG_BUILD_DEBUG_PERF=1 output.
We have reached the end of the series. We followed the path source code takes to the browser from the module graph to the source map: webpack's loader chain, esbuild's speed, Vite's dev server, the Angular CLI's builder, bundle analysis and source maps. Measuring how a smaller bundle affects page speed is the topic of the upcoming Web Performance series; there we will measure this series' Bookstore application through LCP and INP.