All posts
Software

Rust-based JS tooling: adopt it for the consolidation, not the speed

The 10x benchmark is real and mostly beside the point. A worked lint-and-format swap, the three compatibility seams that actually break in production, and a swap-now / wait / leave-it framework for deciding which native tools to trust today.

Dhileep Kumar6 min read
Rust-based JS tooling: adopt it for the consolidation, not the speed

There's a version of the Rust-tooling story that's told everywhere: JavaScript is slow, Rust is fast, therefore your bundler got rewritten and now your CI is quicker. True as far as it goes, and utterly useless for the only decision you actually have to make -- which of these tools to put in your build today, and which to leave alone for another year. This post is about that decision, not the benchmark.

My argument is narrower and more opinionated than the usual take: the speed is real, but speed is not why you should adopt these tools, and speed is not the thing that will bite you when you do. The interesting story is about consolidation and about where the compatibility seams are hidden -- get those two right and the speed comes along for free. That's the whole post: a mental model, one worked migration, the seams that actually break, and a framework for which tools to trust when.

The mental model: it was never about the language you ship

Here's the frame I use. Your build tools are a compiler pipeline. A compiler's job is overwhelmingly parsing, walking a tree, and re-serializing. That workload is embarrassingly parallel and allocation-heavy -- precisely the two things a single-threaded, garbage-collected runtime is worst at. JavaScript is a wonderful language to write an app in and a genuinely awkward one to write a compiler in.

So the rewrite wave is not 'Rust is trendy. ' It's an ecosystem noticing that it had been running its compilers on the one runtime least suited to compiler work, and quietly moving that layer -- and only that layer -- to native code. Nothing about the JavaScript you type changes; the machine underneath just stopped speaking it. And once you hold that model, a second thing falls out that most explainers bury: the win isn't the raw 10x on one tool -- it's that when parsing becomes nearly free, you can stop parsing the same file five times.

The real prize is consolidation, not raw speed

Think about what a typical JavaScript setup does to a single source file on save. ESLint parses it into an AST to lint. Prettier parses it again to format. Babel or tsc parses it again to transpile. Your bundler parses it a fourth time to build the module graph. Four independent parses of the same bytes, each in its own process, each with its own config file, each a separate dependency to keep on speaking terms with the others.

A native tool like Biome or the Oxc family collapses that. Parse once, then lint and format off the same tree in a single pass. The speedup you feel is partly Rust and partly the fact that you deleted three redundant parses and three config files. That second half is available to you as an architectural decision even before you count microseconds.

The bottleneck was never the language you ship. It was that four different tools each re-parsed the same file and none of them talked to each other.

This reframes adoption entirely. Don't ask 'is Biome faster than ESLint plus Prettier? ' -- it is, and it barely matters. Ask 'how many tools, configs, and plugins can I retire by adopting it? ' The config-sprawl reduction is a permanent maintenance dividend. The speed is a one-time nice-to-have you stop noticing after a week.

A worked example: the lint-and-format swap

Let me walk through the single highest-leverage migration concretely, because 'adopt native tools' is not advice, it's a slogan. Say you have a mid-sized repo -- a few thousand files -- with the usual stack: ESLint with a shared config, Prettier, a lint-staged pre-commit hook, and a CI lint job. Four moving parts. Here's the before, in the shape everyone recognizes.

Notice eslint-config-prettier in there. That dependency exists solely to stop ESLint and Prettier from fighting over the same rules -- a whole package whose only job is to referee two tools that each parse your code separately. That is the tax made visible. Now the after:

Four dependencies became one. The referee package is gone because a single tool can't disagree with itself about formatting. The pre-commit hook now runs one binary that lints and formats in one traversal. This is the swap I'd make first in almost any repo, and notice that I have not mentioned a benchmark number once -- the case stands on the dependency graph alone.

The honest catch: your ESLint config almost certainly encodes rules that don't have a one-to-one equivalent yet, especially custom or plugin rules. Which is exactly the seam we need to talk about.

Where the bodies are buried: the seams, not the speed

Every failure mode I'd worry about in this migration lives at a boundary, not inside the fast tool. Three seams are worth naming because none of them show up in a benchmark.

  • The plugin cliff. Native linters ship a curated rule set. If your team leaned on a niche ESLint plugin -- an accessibility ruleset, a framework-specific one, an internal custom rule -- there may simply be no equivalent. You don't discover this from the README; you discover it when a rule you relied on silently stops being enforced. Audit your enabled rules before you switch, not after.
  • The N-API boundary. Rust tools plug into Node through native bindings, and that boundary ships as a prebuilt binary per platform. Most days it's invisible. The day it isn't is when CI runs on an architecture without a prebuilt binary (Alpine containers and ARM runners are the classic offenders) and you get a cryptic native-module error instead of a clean 'unsupported platform' message.
  • The half-migrated graph. During a bundler transition you live in a world where some plugins are native and some are JavaScript shims bridging back to the old API. Those shims are where behavior quietly diverges -- a source map that's subtly off, a loader that handles an edge case differently. The tool is fast; the shim is where correctness leaks.

The pattern: the Rust core is the boringly reliable part. Your risk is entirely in the translation layer between it and the decade of JavaScript convention it's replacing. Budget your migration time there.

A decision framework: swap now, wait, or leave it

Not all rewrites are at the same maturity, and treating them as one wave is how people get burned. I sort any given native tool into one of three buckets by asking a single question: how large is its compatibility surface with the ecosystem it replaces? Small surface, swap today. Enormous surface, wait.

  1. Swap now -- formatters and linters (Biome, Oxc). Tiny compatibility surface: they read your source and emit diagnostics or formatted output. Nothing downstream depends on their internals. Worst case, you revert one dev dependency. Highest leverage, lowest blast radius.
  2. Adopt deliberately -- transpilers and CSS tools (SWC, Lightning CSS). Often already inside your framework, so you may be running them without knowing. Safe, but verify behavior on anything exotic in your syntax or your PostCSS pipeline before you rely on it.
  3. Wait and watch -- bundlers with rich plugin ecosystems (Turbopack, Rspack, Rolldown). Massive compatibility surface: the entire plugin and loader world hangs off the bundler's API. This is where half-migrated graphs and shim bugs live. Adopt when your specific plugins are natively supported, not before.

And the honest 'don't bother' case, which every guide skips: if you have a small project, none of this is for you. Startup and re-parse costs only compound at monorepo scale. On a few hundred files your ESLint run finishes before you've read this sentence, and a migration is pure downside -- you'd spend real hours to shave milliseconds nobody can perceive. Adopt native tooling because you have a config-sprawl or a CI-time problem, not because the internet said Rust.

So do you need to learn Rust? No -- and that's the whole point

You almost certainly never touch the language. That's the design intent: the rewrite happens one layer below your keyboard, and you consume it through config. If you find yourself needing to write Rust to use these tools, something has gone wrong -- either you're contributing to the tool itself, or you've adopted something far too early. My actual recommendation, stripped of hype: make the lint-and-format swap this quarter if you have more than a few hundred files, for the config consolidation and not the speed. Let the transpiler come to you through your framework. Keep the bundler on a watch-list and move only when your plugins have native homes. Before any swap, spend an hour auditing which rules and plugins you truly depend on -- that audit, not the benchmark, is what determines whether the migration is painless or a week of yak-shaving.

The era of writing your build tools in the language they build is ending, and it's ending for a good structural reason rather than a fashionable one. The keyboard still types JavaScript. The machine underneath just stopped speaking it -- and the only real skill the transition asks of you is knowing where the two still have to talk.

Share

Enjoyed this?

Get the next deep dive in your inbox. No spam — just the stories worth reading.

Subscribe to the newsletter

Comments