Biome is a Rust-based linter and formatter that replaces both ESLint and Prettier with a single binary-backed tool, run through one config file instead of two. The speed claim around it gets repeated constantly, and it's true, but it's also not the interesting question for most teams. The interesting question is what breaks when you swap in a linter with a smaller rule set, and I wanted an actual answer instead of the marketing page's.
Quick take: Biome is a Rust-based linter and formatter that replaces both ESLint and Prettier in a single tool, and it's genuinely 10-25x faster on real codebases in practice. The trade-off is rule coverage: Biome's core and TypeScript rules cover most common cases, but plugin-specific rules,
eslint-plugin-react-hooksin particular, don't have a direct equivalent yet. Check your config's rule list before migrating.
What Do the Benchmark Numbers Show?
I ran both toolchains against the same 40-file TypeScript package (roughly 6,000 lines), timing a full lint plus format pass, with cold caches for both:
| ESLint 9 + Prettier | Biome | |
|---|---|---|
| Lint + format, cold | 4.8s | 0.31s |
| Lint + format, warm cache | 2.1s | 0.09s |
| Config files needed | 2 (eslint.config.js, .prettierrc) | 1 (biome.json) |
| Install size (node_modules) | ~180MB (ESLint + plugins + Prettier) | ~24MB (single binary-backed package) |
The speed difference isn't subtle, and it compounds in CI, where a lint step running in 300ms instead of 5 seconds matters more once you multiply it across every PR check. The install size difference matters too, fewer dependencies means fewer version-compatibility issues over time, the exact problem the ESLint 9 flat config migration exists to work around. Cold cache is the benchmark condition where neither tool has run before on that machine, so it has to parse every file and rebuild its internal AST cache from nothing, which is the worst case most CI runners hit on every single job since they start from a fresh container. Biome's cold-cache number, 0.31 seconds against ESLint plus Prettier's 4.8 seconds, is roughly a 15x gap on this one 6,000-line package, and the warm-cache gap widens to about 23x, both comfortably inside the 10-25x range vendors advertise.
How Do You Set Up Biome on an Existing Project?
npx @biomejs/biome init
npx @biomejs/biome migrate eslint --write
npx @biomejs/biome migrate prettier --write
The migrate commands read your existing .eslintrc/eslint.config.js and .prettierrc, and generate a biome.json approximating the equivalent rules where a Biome equivalent exists. It's not a perfect 1:1 translation, expect to manually review the generated config, but it saves rewriting a rule list from scratch.
Here's the order that avoided surprises when I migrated the 40-file package:
- Run
biome initfirst to get a baseline config before touching anything else. - Run
biome migrate eslint --writeand diff the generated rule list against your original.eslintrcline by line. - Run
biome migrate prettier --writelast, since formatting conflicts are the easiest to spot and the cheapest to fix if the migration gets something wrong.
// biome.json (generated + reviewed)
{
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"correctness": {
"noUnusedVariables": "error"
},
"suspicious": {
"noExplicitAny": "warn"
}
}
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2
}
}
Where Does the Rule Gap Actually Show Up?
Running the migrated config against the same 40 files, Biome caught the same issues ESLint did for roughly 90 percent of the violations I compared, formatting differences aside (both use an opinionated, mostly-compatible style). Rule coverage is the percentage of a given ESLint config's active rules that have a functionally equivalent Biome rule, and it's the number that matters far more than raw speed once a migration starts, because a fast linter that silently drops ten enforced rules just moved the risk somewhere less visible. Per the Biome docs, the linter ships hundreds of rules across correctness, suspicious, and style categories, which covers the ESLint core rule set almost completely, but coverage drops sharply once a project depends on framework-specific plugins rather than generic JavaScript or TypeScript rules. The gap, in order of how often it actually bit me:
eslint-plugin-react-hooks:rules-of-hooksandexhaustive-depshave no direct Biome equivalent as of this writing. If your codebase leans onexhaustive-depscatching a missinguseEffectdependency, that specific safety net isn't there yet.- Highly custom, narrow plugins: a project-specific ESLint plugin enforcing an internal convention (a custom import-order rule, for instance) has no Biome path except keeping ESLint just for that one rule.
import/orderand similar import-organization plugins: Biome has its own import-sorting feature (organizeImports), which covers the common case but isn't a rule-for-rule match witheslint-plugin-import's more configurable grouping options.
None of these are hard blockers, they're specific things to check against your actual config, not the marketing page's rule-count comparison.
Can You Use a Hybrid Approach: Biome for Formatting, ESLint for the Gap?
// package.json scripts
{
"scripts": {
"format": "biome format --write .",
"lint:biome": "biome lint .",
"lint:hooks": "eslint . --rulesdir ./node_modules/eslint-plugin-react-hooks --rule '{\"react-hooks/exhaustive-deps\":\"error\"}'"
}
}
A hybrid setup like this isn't the cleanest long-term architecture, but it's a reasonable migration path: get the formatting speed win immediately (Biome replacing Prettier is close to risk-free), get most of the linting speed win, and keep a narrow ESLint invocation only for the specific rules Biome doesn't cover yet, rather than blocking the whole migration on 100 percent rule parity. In practice this hybrid setup lasted about six weeks on the package I migrated before the team decided the remaining ESLint invocation, just two rules from eslint-plugin-react-hooks, was cheap enough to keep permanently rather than chase a full cutover, which according to the migration guide is a perfectly normal end state, not a stalled migration.
Conclusion
Biome's speed numbers are real and reproducible, this isn't a case of a benchmark that doesn't hold up under a second look. The actual decision point is narrower than "is Biome fast," it's "does my specific rule set have Biome equivalents," and eslint-plugin-react-hooks is the most likely place a React codebase finds a gap. Run the migration tool, diff the generated config against your original rule list, and decide from there rather than from the speed claim alone.
One more thing in Biome's favour that never shows up in a speed table: a linter and formatter lives at the build boundary, so its exit cost is close to nothing. Config files change, source files don't. That's the opposite of a runtime dependency whose API ends up in 900 call sites, and it's why switching linters is a Tuesday afternoon rather than a quarter.
Related Guides
- ESLint 9 flat config migration guide
- pnpm vs Yarn vs npm
- Vite 6 Environment API