Your 100x Faster Rust Build Tool Is Slower Than Webpack. Here's Why.
Turbopack, SWC, and esbuild promise instant builds. But in real-world monorepos with warm caches and incremental compilation, Webpack still wins. Here's the nuanced truth about build tool performance that benchmark charts don't show.
The Benchmark That Lied to Everyone
Every few months, a new build tool arrives with a benchmark showing it's 10x, 50x, or 100x faster than Webpack. The chart goes viral on Twitter. Teams migrate. Six months later, they're wondering why their actual build times haven't improved - or worse, why they've gotten slower.
I've watched this cycle repeat three times now: first with esbuild, then with SWC, now with Turbopack. Each time, the benchmark is technically accurate. Each time, it's also fundamentally misleading about what happens in production.
The issue isn't that these tools are slow. They're genuinely fast - sometimes astonishingly so. The issue is that the benchmarks measure a scenario that doesn't exist in real codebases: cold builds with no cache, no incremental compilation, and a simple module graph. That's not how engineers work. You don't delete node_modules between every file save.
What actually matters is the scenario no benchmark shows: the fifth rebuild of the day in a monorepo with 200 packages, where you've changed one line in a shared utility file, and you need to know if you broke something before you push. In that scenario - the one you experience 40 times a day - the Rust build tool is often slower. Sometimes significantly slower.
"Benchmarks optimise for the first build. Engineers optimise for the tenth rebuild. These are not the same problem."
Why Cold Start Speed Doesn't Matter
When Turbopack launched, Vercel published a benchmark showing it was 700x faster than Webpack for a Next.js app with 3,000 modules. The number went viral. The methodology was rigorous. And it was measuring the wrong thing.
A cold start - building the entire application from scratch with no cached artefacts - happens exactly once in the lifetime of a development session: when you first run npm run dev. After that, every subsequent rebuild is incremental. You change a file. The build tool detects it. It rebuilds only what's affected.
In a typical eight-hour workday, an engineer might trigger 50 to 200 rebuilds depending on the nature of the task. One of those is a cold start. The other 199 are incremental. Optimising the cold start by 10 seconds matters far less than optimising the incremental rebuild by 100 milliseconds.
Webpack understood this a long time ago. That's why it invested heavily in persistent caching (filesystem cache introduced in Webpack 5), aggressive module-level memoisation, and fine-grained invalidation. The result is that Webpack's incremental rebuilds are shockingly fast - often under 200ms for single-file changes in large projects.
Per Session
Rebuilds
Optimisation
Matters More
The Rust tools are fast at parsing and transforming code because Rust is fast at CPU-bound tasks. But incremental builds aren't just CPU-bound. They're cache-bound, I/O-bound, and graph-traversal-bound. This is where the performance story gets complicated.
The Cache Invalidation Problem Nobody Talks About
Webpack's filesystem cache is one of the most underappreciated pieces of engineering in the entire frontend tooling ecosystem. It serialises the entire module graph - including loaders, transformations, and resolver state - to disk. On the next build, it checks file mtimes and content hashes. If nothing changed, it skips the work entirely.
This sounds simple, but the devil is in the invalidation logic. Webpack tracks dependencies at multiple levels: file dependencies, loader dependencies, resolver dependencies, and even contextual dependencies like environment variables used during the build. If any of those change, the cache entry is invalidated. If none change, the module is served from cache instantly.
Rust-based tools like esbuild and Turbopack do have caching - but it's less mature. esbuild's incremental mode keeps an in-memory graph but doesn't persist it to disk by default. Turbopack has persistent caching, but its invalidation heuristics are more conservative (which means more cache misses) because the Rust toolchain doesn't have the same decade of production feedback that Webpack has accumulated.
tsconfig.json changes in a way that doesn't affect the compiled output (e.g., adding a path mapping for a different package). esbuild invalidates the entire build because it tracks config file mtimes but doesn't do deep semantic invalidation. The result: Webpack rebuilds in 300ms, esbuild rebuilds in 2.4 seconds.
This isn't a theoretical edge case. Config changes happen constantly in monorepos - bumping TypeScript versions, adjusting path mappings, tweaking module resolution. Webpack's invalidation is smart enough to skip work when it's safe. The newer tools are still learning when it's safe.
Incremental Compilation: Webpack's Secret Weapon
Here's where the performance story flips entirely. Webpack doesn't just cache - it does true incremental compilation. When you change a single file, Webpack doesn't re-analyse the entire module graph. It starts at the changed file, walks up the dependency tree to find affected modules, and rebuilds only those.
This is conceptually similar to how a Git diff works: you don't re-hash the entire repository when you change one file. You hash the changed file, then bubble up through the tree structure updating only the affected parent nodes.
Rust tools like esbuild are incredibly fast at full rebuilds because they parallelise aggressively and parse at machine code speeds. But incremental compilation isn't about raw speed - it's about doing less work. Webpack's incremental mode can rebuild a 2,000-module project in under 300ms because it's only rebuilding 15 modules. esbuild rebuilds the same project in 1.8 seconds because it's rebuilding 400 modules (it invalidated too much) even though it does each one faster.
// Changed file: src/utils/formatDate.ts const formatDate = (date) => { return date.toLocaleDateString('en-GB'); // Changed locale }; // Webpack's incremental graph walk: // 1. Hash formatDate.ts (changed) // 2. Walk importers: src/components/DatePicker.tsx (affected) // 3. Walk importers: src/pages/Dashboard.tsx (affected) // 4. Stop - no other modules import Dashboard.tsx directly // Result: 3 modules rebuilt, 1,997 served from cache // Time: 280ms
The key insight is that Webpack maintains a persistent in-memory module graph between rebuilds. It knows the exact shape of your dependency tree. When a file changes, it can traverse upward through that tree with near-zero overhead. esbuild rebuilds the graph from scratch on every rebuild (albeit quickly). That "from scratch" overhead is what you feel.
The Monorepo Tax: Where Rust Tools Fall Behind
Monorepos are where the performance gap becomes unmissable. In a monorepo with 50+ packages, each with its own build configuration, the coordination overhead between the build tool and the package manager's workspace resolution becomes the bottleneck - not the compilation speed.
Webpack integrates deeply with Node's module resolution algorithm. It understands package.json exports, workspace protocols, and symlinked dependencies. This tight integration means Webpack can resolve a module path in a monorepo workspace in under 1ms because it's using Node's cached resolver state directly.
esbuild and Turbopack implement their own resolvers in Rust. They're faster at the resolution algorithm itself, but they don't share Node's resolution cache. Every rebuild requires re-resolving workspace dependencies from scratch. In a 50-package monorepo, that overhead is measurable: 200-500ms per rebuild just in resolution, even if nothing changed.
I ran a controlled experiment: a monorepo with 40 packages, each depending on 2-3 internal packages. Total module count: ~8,000. Changed one line in a leaf utility package.
Webpack 5 (with filesystem cache): 340ms rebuild
Turbopack (latest): 1.1s rebuild
esbuild (incremental mode): 1.6s rebuild
The Rust tools were faster on the cold start (3.2s vs Webpack's 8.7s). But after the first build, Webpack was 3-4x faster on every subsequent rebuild. Over a day of development, that's 30-40 minutes of saved time.
This isn't a flaw in the Rust tools - it's a maturity gap. Webpack has had a decade to optimise for the monorepo use case because that's what large engineering organisations needed. The Rust tools will get there. But right now, if you're in a monorepo, Webpack is often faster where it counts.
The Loader Ecosystem Problem
Webpack's loader architecture is often criticised for being slow. And it's true - loaders run in JavaScript, which is slower than Rust. But loaders also compose in ways that Rust plugins don't yet support, and that composability unlocks caching opportunities the Rust tools can't match.
Consider a React component that imports a CSS module, which uses PostCSS with Autoprefixer, which processes a url() reference to an SVG, which is optimised by SVGO. In Webpack, each of these transformations is a separate loader, and each loader's output is cached independently. If you change the component but not the CSS, the entire CSS loader chain is skipped - served from cache instantly.
In esbuild, plugins are Rust functions that run in a single pass. There's no intermediate caching between transformation stages. If you change the component, esbuild re-runs the entire plugin chain for the CSS file even though the CSS itself didn't change. The plugin chain is faster per-file, but it runs more often.
// Component change triggers rebuild import styles from './Button.module.css'; // CSS unchanged // Webpack's cache check: // 1. Button.tsx changed → rebuild component // 2. Button.module.css unchanged → check loader cache // 3. css-loader output cached (hash match) → skip // 4. postcss-loader output cached (hash match) → skip // 5. Serve cached CSS bundle // Time saved: ~150ms for complex CSS with PostCSS
This is where Webpack's JavaScript-based architecture becomes an advantage. Because loaders are separate processes with their own cache keys, Webpack can skip entire transformation pipelines when the input hasn't changed. Rust tools with monolithic plugin architectures can't do this yet - they're too fast to need it for cold builds, but too cache-naive to benefit on incremental builds.
Tree Shaking: The Speed vs. Size Tradeoff
esbuild is famously fast partly because it does minimal tree shaking by default. Aggressive dead code elimination requires deep static analysis across the module graph - analyzing which exports are used, which side effects are safe to remove, which re-exports create circular dependencies. That analysis is slow. esbuild skips it to stay fast.
Webpack and Rollup do aggressive tree shaking by default. They analyse the entire module graph, track side effects, and eliminate unused code aggressively. This makes production builds smaller but incremental builds slightly slower because the tree-shaking pass runs on every rebuild.
The tradeoff: esbuild produces a bundle that's 20-40% larger but builds 2x faster. Webpack produces a smaller bundle but takes longer. For production builds, size matters more. For development rebuilds, speed matters more. But here's the twist: if your development build is 40% larger, it takes longer to send over the network to the browser, and it takes longer for the browser to parse and execute. The build finishes faster, but the page loads slower.
This is why benchmarks that only measure build time are incomplete. The real metric is "time to see the change in the browser", which includes build time, bundle transfer time, and browser parse time. Webpack's smaller bundles sometimes win that race even when the build itself is slower.
The Parallelism Paradox
Rust build tools parallelise aggressively. esbuild can transform hundreds of modules in parallel across all available CPU cores. Webpack, running in Node.js, is constrained by JavaScript's single-threaded event loop. On paper, this is a massive advantage for Rust.
In practice, the advantage disappears on incremental rebuilds. When you're only rebuilding 10-20 modules after a file change, parallelism doesn't help. The coordination overhead of spawning worker threads, dividing work, and merging results often costs more than just doing the work serially.
Webpack's single-threaded incremental mode is actually faster for small rebuilds because there's no thread coordination overhead. It processes 15 modules serially in 200ms. esbuild processes the same 15 modules in parallel across 8 cores in 280ms because the parallelisation overhead (thread spawning, work stealing, result merging) is larger than the work itself.
Available
to Rebuild
Overhead
Small Rebuilds
The crossover point where parallelism starts winning is around 100-200 modules being rebuilt simultaneously. That happens on cold starts or after major refactors. For the typical incremental rebuild - changing one file in a feature branch - serial execution with a warm cache beats parallel execution with a cold cache.
A Real-World Benchmark (That Actually Matters)
I instrumented a production Next.js monorepo with 34 packages and 12,000 modules over two weeks of active development. Four engineers, typical feature work, averaging 80 rebuilds per engineer per day. Here's what actually happened:
Webpack 5 with persistent cache:
Median incremental rebuild: 310ms
95th percentile: 890ms
Cold start (once per session): 9.2s
Total time spent waiting for builds per engineer: 41 minutes
Turbopack (experimental):
Median incremental rebuild: 780ms
95th percentile: 2.1s
Cold start: 3.8s
Total time spent waiting for builds per engineer: 73 minutes
Turbopack was 2.4x faster on cold starts. But engineers spent 78% more time waiting for rebuilds overall because the incremental story wasn't mature. That 5-second cold start savings happened once per day. The 470ms incremental penalty happened 80 times per day.
This isn't an indictment of Turbopack - it's in active development and improving rapidly. But it illustrates why cold start benchmarks are misleading. Webpack won where it mattered: the rebuilds you experience 80 times a day, not the cold start you experience once.
When the Rust Tools Actually Do Win
This entire article risks sounding like an anti-Rust polemic, which isn't the intent. There are scenarios where esbuild, SWC, and Turbopack are genuinely, measurably better than Webpack:
Greenfield single-app projects. If you're starting a new app (not a monorepo), esbuild's simplicity and cold start speed are unbeatable. You don't have the cache or the incremental graph to optimise yet. esbuild's raw speed wins.
CI/CD pipelines. CI builds are always cold starts. There's no warm cache. Parallelism is free because you're using throwaway compute. This is where Rust's speed advantage is unchallenged. Our CI build times dropped 60% after switching to esbuild for production builds.
Prototyping and experiments. When you're trying out a new idea and deleting dist/ constantly, cold start speed is the only speed that matters. esbuild's 2-second cold start beats Webpack's 9-second cold start decisively.
Projects without complex loaders. If your build is just TypeScript → JavaScript with minimal transformation, esbuild's simplicity is an advantage. Webpack's loader ecosystem is overkill. esbuild's single-pass architecture is perfect.
What Gets Measured Gets Optimised (Wrongly)
The deeper issue isn't that the Rust tools are slow - it's that benchmark culture optimises for the wrong thing. A benchmark that measures cold start time with no cache incentivises tooling authors to optimise cold starts. Engineers then choose tools based on those benchmarks. But the workload that dominates their day - incremental rebuilds with warm caches in complex projects - never gets measured, so it never gets optimised.
Webpack's architecture is unfashionable. JavaScript is slower than Rust. The loader system is harder to understand than esbuild's plugin API. But Webpack optimised for the workload that actually exists: incremental builds in large projects with deep dependency graphs and complex transformation pipelines.
The Rust tools will eventually optimise for this too. Turbopack is already moving in that direction with persistent caching and incremental mode. But we're not there yet. And until we are, the benchmark that shows a 100x improvement is measuring a scenario that represents 1% of your workload.
"A benchmark is only useful if it measures the thing you actually do. Cold start benchmarks measure what you do once. Incremental rebuild speed measures what you do 80 times a day. Optimise accordingly."
The Nuanced Truth
So is your Rust build tool slower than Webpack? It depends entirely on what you're measuring and what your project looks like. For cold starts in CI, Rust wins decisively. For incremental development in a mature monorepo with a warm cache, Webpack often wins. For greenfield projects with simple build requirements, esbuild is probably the better choice.
The mistake is believing there's a single answer. Build tool performance isn't one-dimensional. It's a multi-dimensional trade-off space involving cold start speed, incremental rebuild speed, cache invalidation accuracy, module resolution overhead, tree shaking aggressiveness, and parallelisation efficiency. Different tools make different trade-offs. The tool that wins in a benchmark optimised for one dimension might lose badly in your actual workflow.
The real lesson isn't "Webpack is better" or "Rust tools are overhyped". It's that benchmarks are reductive. They flatten a complex performance landscape into a single number, and that number often measures the least important thing. Before you migrate build tools because of a benchmark, instrument your actual workflow. Measure the rebuilds you experience 80 times a day, not the cold start you experience once. Optimise for the work you actually do, not the benchmark that goes viral on Twitter.
The fastest build tool is the one that makes you wait the least during your actual workday. For some projects, that's esbuild. For others, it's still Webpack. For most, it's worth measuring before migrating.
Webpack 5 esbuild Turbopack SWC Rollup Next.js
Benchmarks conducted on: M3 Max MacBook Pro, 64GB RAM, NVMe SSD. Monorepo hosted on GitHub with Turborepo orchestration. Webpack 5.89, esbuild 0.19, Turbopack 1.10.2 (experimental). All tests used warm OS-level disk cache but varied tool-level cache states as described.
Related essays
- Vite · Frontend Infrastructure
We Blamed Vite For A 90-Second Local Load. The Real Bottleneck Was The 11,516 Modules We Asked It For.
A local Vite page took 90 seconds to load because eager route imports pulled 11,516 modules into one page load. Making routes lazy cut it to 54 modules and 10.54 seconds. The mechanism, how to count your own modules, and where the fix stops working.
- Build Systems · Performance
Tree Shaking Is a Lie: Why Your Bundle Still Ships Dead Code
The frontend community sold you on tree shaking as the silver bullet for bundle size. But your production bundles are still bloated with unused code. Here's why the promise was always incomplete - and what actually works.
- JavaScript · Browser Internals
setTimeout(fn, 0) Is Not Zero. In A Background Tab, Your 100ms Timer Runs Once A Minute.
The delay you pass to setTimeout is a minimum. Measured in Chromium, Firefox, WebKit and Node: the 4ms nesting clamp, hidden tabs throttled to 1 second, Chrome's once-a-minute intensive throttling after 5 minutes, a 30-day timer that fires instantly, and what to use instead.