Engineering Diaries · Build Tools
§
Build ToolsWebpackRust

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.

Code editor with build tools
01 · The Problem

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."

02 · Cold Start Illusion

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.

1
Cold Start
Per Session
199
Incremental
Rebuilds
10s
Cold Start
Optimisation
100ms
Incremental
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.

03 · The Cache Problem

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.

Real-World Example In a monorepo with shared TypeScript configs, Webpack correctly caches modules even when 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.

04 · Incremental Builds

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.

Webpack Incremental Rebuild - Single File Change
// 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.

05 · Monorepo Tax

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.

The Benchmark Nobody Publishes

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.

06 · Loader Ecosystem

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.

Webpack Loader Chain - Cached Stages
// 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.

07 · Tree Shaking

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.

The Network Bottleneck In a remote development setup (e.g., GitHub Codespaces, AWS Cloud9), the bundle size difference between esbuild and Webpack can outweigh the build time difference. A 40% larger bundle over a 100ms RTT network adds 300-500ms to hot reload time - erasing esbuild's 200ms build advantage entirely.

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.

08 · Parallelism

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.

8
CPU Cores
Available
15
Modules
to Rebuild
40ms
Parallelisation
Overhead
Serial
Faster for
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.

09 · Real-World Data

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:

Production Monorepo - 14 Days of Development

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.

10 · When Rust Wins

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.

The Real Answer Use esbuild/SWC for CI builds and production bundling. Use Webpack for local development in mature projects with warm caches. Use Turbopack if you're on Next.js and can tolerate experimental tooling. Don't migrate just because a benchmark chart says a tool is faster - measure your actual workflow.
11 · What Gets Measured

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."

12 · Conclusion

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.

· · ·
Tools Referenced

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.

All benchmark data is from real production environments and controlled experiments. Tool versions and configurations available on request. Performance characteristics may vary significantly based on project structure, hardware, and workload patterns.