Skip to main content

How JavaScript Bundlers Work: A Field Guide from 2012 to 2026

· 14 min read
Pere Pages
Software Engineer
Editorial illustration of scattered code modules flowing along conveyor belts into a funnel that outputs a few neat, bundled packages

Bundlers are the most invisible critical infrastructure in frontend development. Every npm run dev and every continuous-integration (CI) pipeline runs through one, yet most of us only think about them when something breaks or when a migration guide lands in our feed. This post is an attempt to fix that: what a bundler actually does under the hood, how we got from script tags to Rust toolchains, which tool owns which niche today, and where the whole thing is heading.

What a bundler actually does

Strip away the branding and every bundler — Webpack, Rollup, esbuild, Rolldown, Turbopack — performs the same fundamental pipeline. Every bundler runs the same six-stage pipeline; understanding it makes every config file and every error message far less mysterious.

1. Entry and resolution

The bundler starts from one or more entry points (src/main.tsx) and asks a deceptively hard question: for every import statement, which file on disk does this refer to? Resolution has to handle relative paths, node_modules lookup, the exports field in package.json, conditional exports (import vs require vs browser), path aliases from tsconfig.json, and extension inference. A surprising fraction of bundler bugs and slowness historically lived right here.

2. Loading and transformation

Once a file is resolved, it usually isn't plain JavaScript the browser can run. TypeScript needs its types stripped, JSX (JavaScript XML) needs compiling to function calls, Sass needs compiling to CSS (Cascading Style Sheets), images need hashing and emitting. This is the layer where the plugin/loader ecosystem lives: each file passes through a chain of transforms until it becomes something the bundler can analyze. It's also where the industry's biggest performance shift happened — replacing JavaScript-based transformers (Babel) with native ones, such as esbuild, SWC (the Speedy Web Compiler), and Oxc (the Oxidation Compiler), made this step 10–100x faster.

3. Building the module graph

As each file is transformed, the bundler parses it into an AST (Abstract Syntax Tree), extracts its imports and exports, and recursively repeats the process. The result is the module graph: a directed graph where nodes are modules and edges are dependencies. Everything else a bundler does — tree shaking, code splitting, Hot Module Replacement (HMR) — is a graph operation on this structure. When people say a bundler "understands your app," this graph is what they mean.

4. Tree shaking (dead code elimination)

Because ES modules have static syntax (import/export can't be conditional or dynamic in their basic form), the bundler can prove which exports are never used and drop them. This is why ESM (ECMAScript Modules) won over CommonJS — the older require()-based module format — as the authoring format: require() calls can be computed at runtime, which makes static analysis nearly impossible. Good tree shaking also depends on side-effect analysis — the "sideEffects": false flag in package.json is you telling the bundler "trust me, importing this file does nothing but define exports," which unlocks much more aggressive elimination.

5. Chunking and code splitting

A naive bundler would concatenate the whole graph into one file. Real ones split it: every dynamic import() becomes a split point, shared dependencies get hoisted into common chunks, and vendor code gets separated from app code so that a one-line change in your app doesn't invalidate the cached 2 MB of node_modules in your users' browsers. Chunking strategy is where bundle-size expertise actually lives — it's an optimization problem balancing number of requests, cache hit rates, and duplicate code across chunks.

6. Code generation

Finally the bundler serializes chunks back into JavaScript files. This includes scope hoisting (merging modules into a single scope instead of wrapping each in a function, which Rollup pioneered), generating the runtime glue that loads chunks on demand, emitting source maps, and minification — renaming variables, removing whitespace, folding constants.

The dev-mode twist: HMR

Development mode inverts the priorities. You don't care about optimal output; you care about seeing your change in under 100 ms. Hot Module Replacement works by keeping the module graph alive in memory: when a file changes, the bundler recompiles only that module, pushes it over a WebSocket, and the runtime swaps it in place — walking up the graph until it finds a module that knows how to accept the update (a React component boundary, a CSS file). The gap between "how dev serves code" and "how prod bundles code" is the source of an entire class of bugs, and closing that gap is one of the defining themes of the current era.

The chronology: how we got here

The fifteen-year arc from hand-ordered script tags to unified Rust toolchains is easier to hold as a single timeline than as a wall of dates.

Pre-2012 — the script tag era. No modules, no bundlers. You ordered <script> tags by hand and everything lived on window. Tools like Grunt and Gulp automated concatenation and minification, but they were task runners, not bundlers — they had no concept of a module graph.

2012–2015 — Browserify and the birth of Webpack. Browserify's insight was letting you write Node-style require() in browser code. Webpack (2012, exploding with React's rise around 2015) generalized this: everything is a module — CSS, images, fonts — and loaders transform anything into the graph. Code splitting and, later, HMR made it the default for a decade.

2015–2017 — Rollup and the ESM bet. Rollup bet on ES modules before browsers even shipped them, and used their static nature to invent tree shaking and scope hoisting. It produced dramatically cleaner output than Webpack, which made it the standard for libraries — a niche it held for years — while Webpack kept apps.

2016–2018 — the zero-config reaction. Webpack config had become a genre of suffering. Parcel launched with "zero configuration" as the whole pitch, and Create React App succeeded by hiding Webpack entirely. The lesson stuck: developers wanted the power without the config file.

2020 — the native-speed rupture. esbuild, written in Go, bundled real projects 10–100x faster than the JavaScript-based competition and made everyone realize the bottleneck was the implementation language, not the algorithms. SWC did the same for the Babel transform layer. The ceiling on JavaScript-based tooling was suddenly visible.

2020–2024 — Vite and the unbundled dev server. Vite's move was architectural rather than raw speed: in development, don't bundle at all. Serve native ES modules directly to the browser, transform files on demand with esbuild, and only bundle for production (with Rollup). Cold starts became near-instant regardless of app size. Vite became the substrate for an entire generation of frameworks — SvelteKit, Astro, Nuxt, SolidStart, Remix — and, notably for testing, Vitest reused the exact same transform pipeline, which is why your Vite config and your Vitest config can share a resolution and plugin story instead of maintaining a parallel Jest/Babel universe.

2022–2024 — the Rust rewrites. Vercel announced Turbopack as Webpack's successor (Tobias Koppers, Webpack's creator, works on it). ByteDance shipped Rspack, a Rust reimplementation of Webpack's API (application programming interface). And VoidZero (Evan You's company) started Rolldown, a Rust bundler with Rollup's plugin API, explicitly built to become Vite's engine.

2025–2026 — consolidation. This is where we are now, and it's been a big eighteen months. Vite 8 shipped in March 2026 with Rolldown as its single, unified Rust-based bundler, replacing the esbuild + Rollup pair — the most significant architectural change since Vite 2. Rolldown itself hit 1.0 stable in May 2026, with its public API locked under semver. Turbopack became the default for production builds in Next.js 16, and Rspack hit 2.0 in April 2026 with modern defaults, followed by 2.1 in June adding a Rust-based React Compiler and import.meta.glob support.

The niches: who owns what

The landscape hasn't consolidated into one winner — it has stratified into roles, each surviving tool owning a territory defined less by benchmarks than by what you're migrating from and what shape your project is.

App bundling for new projects → Vite. The default, and by a wide margin. Framework-agnostic, enormous plugin ecosystem, and now a single Rust engine for dev and prod. Vite is downloaded around 65 million times a week, and virtually every non-Next meta-framework builds on it.

Legacy Webpack codebases → Rspack. Rspack is explicitly designed as a webpack-compatible drop-in replacement — existing webpack configs, plugins, and loaders work with minimal changes. If you have an 800-line webpack.config.js, custom loaders, and Module Federation in production, this is the pragmatic path: migration typically takes one to two days, versus the multi-week effort of a full Vite migration. Discord, Microsoft, and Amazon teams have taken this route.

Next.js → Turbopack. Excellent inside its boundary, essentially nonexistent outside it. It's not a general-purpose bundler you'd drop into an arbitrary project — it's Next.js infrastructure. If you're on Next.js you're already using it; if you're not, it's not relevant to your decision.

Library bundling → Rollup, transitioning to Rolldown. Rollup's clean ESM output made it the library standard for a decade; Rolldown inherits that role with its Rollup-compatible API, and tools like tsup/tsdown wrap this niche for TypeScript libraries.

The transform layer → esbuild, SWC, Oxc. These aren't competing for the bundler slot anymore — they're the engines inside other tools. esbuild remains the fastest raw transformer, SWC powers Next.js and Rspack, and Oxc (parser, resolver, transformer, minifier) now powers Vite 8, having quietly replaced esbuild's role there.

Webpack itself → maintenance-mode incumbent. Still everywhere in enterprise, still maintained, still used by 86% of developers according to the State of JavaScript (State of JS) 2025 survey — but liked by only 14%, and declining fast in new-project adoption.

Comparison at a glance

Vite 8 (Rolldown)Rspack 2.xTurbopackWebpack 5
LanguageRust (Rolldown + Oxc)RustRustJavaScript
Plugin APIRollup-compatibleWebpack-compatibleNext.js-internalWebpack
Dev modelUnbundled ESM (full-bundle mode coming)Bundled, incrementalBundled, function-level incrementalBundled
Best forNew projects, Vite-based frameworksWebpack migrations, Module FederationNext.js onlyDeep legacy plugin dependencies
Prod buildsVery fast (10–30x vs Rollup)Very fast (~5–23x vs webpack)Fast, Next-onlySlow
EcosystemLargest for modern tooling~85% webpack plugin compatClosedLargest overall (5,000+ plugins)

Very fast Fast Slow

Two caveats to any benchmark table. First, performance gains scale with project size — small projects see 2–5x, large ones with 500+ modules see the headline 10–30x numbers. Second, all the Rust tools now deliver sub-200 ms HMR, which is functionally instant; the differentiator in day-to-day work is consistency and ecosystem, not raw speed.

The current situation (mid-2026)

Three threads define this moment.

The dev/prod split is closing. Vite's original architecture had two personalities: esbuild in dev, Rollup in prod, with an accumulating pile of glue code and edge cases where the two disagreed. Vite 8 replaces both with one engine, so dev and prod use identical module resolution and transform paths — the "works in dev, breaks in prod" class of bugs becomes much rarer. For anyone who has debugged a CommonJS interop difference between vite dev and vite build, this is arguably a bigger deal than the speed.

Real-world numbers are dramatic, with a real caveat. Linear's production build dropped from 46 seconds to 6; GitLab went from 2.5 minutes to 22 seconds versus Vite 7. The migration path is deliberately gradual: swap in the rolldown-vite package on Vite 7 first to isolate bundler-specific breakage, then upgrade to Vite 8. The caveat worth knowing before your CI runner falls over: Vite 8's dev process currently uses roughly 7x more random-access memory (RAM) than Vite 7, because Rolldown keeps more of the module graph in memory.

Bundlers now have corporate landlords. Rspack is ByteDance, Turbopack is Vercel, and in June 2026 Cloudflare acquired VoidZero — the company behind Vite, Vitest, Rolldown, and Oxc — committing a million dollars to a Vite ecosystem fund with a promise everything stays open source. This is mostly good (funded maintainers, faster development) but it changes the risk calculus: pin your versions, and keep an eye on governance.

What's coming

Full bundle mode for dev. The irony of the era: Vite won by not bundling in dev, and its next move is to bundle in dev again — because on very large apps, thousands of unbundled ESM requests became the new bottleneck. With Rolldown fast enough to bundle the whole app at dev speed, preliminary numbers show 3x faster dev startup, 40% faster full reloads, and 10x fewer network requests. The unbundled experiment was right for 2020's hardware and wrong for 2026's codebases, and it took a Rust bundler to make bundled dev fast enough to come back.

Smarter output, not just faster pipelines. With speed largely solved, the frontier moves to what gets emitted. Expect deeper cross-module analysis (bundler and compiler sharing semantic information — Vite is already leveraging Oxc's analysis for better tree shaking in Rolldown), lazy barrel optimization so that a barrel file no longer forces compiling every module it re-exports, and module-level persistent caching so warm builds approach zero.

The unified toolchain. The direction VoidZero has been explicit about: one Rust foundation under parsing, resolving, transforming, linting, formatting, bundling, and testing — Oxc + Rolldown + Vite + Vitest as a coherent stack, the JavaScript equivalent of what Cargo is to Rust. Rspack is building the mirror image with Rsbuild, Rslib, and Rsdoctor. The unit of adoption is shifting from "a bundler" to "a toolchain."

Turbopack's open question. Whether Vercel ever invests in making it genuinely framework-agnostic, or whether it remains excellent Next.js plumbing. As of mid-2026, every signal points to the latter.

Choosing in practice

The decision has become refreshingly boring, and that's the healthiest sign the ecosystem has shown in a decade. Pick by migration path, not by benchmark.

All of them are fast now. The best build tool in 2026 is the one you configure once and stop thinking about — which, after fifteen years of loader chains and config archaeology, is exactly what we always wanted.

References