Skip to content

Is Biome Ready to Replace ESLint and Prettier?

A real benchmark of Biome against ESLint and Prettier on the same TypeScript monorepo, plus a rule-coverage check for what Biome still doesn't catch.

· · 9 min read

Updated: October 9, 2026

A 3D code brackets symbol on a soft gradient

Quick Take

On a 13,810-line TypeScript sample, Biome 2.5.14 did lint plus format in about 0.2 seconds against 1.9 seconds cold for ESLint 9.39.5 plus Prettier 3.9.9, roughly 9x. ESLint 10 is the current major now (10.12.0), and I timed the 9.x line, so treat the multiplier as a 9.x number. Coverage is the better question. Biome's own migration report mapped 81 of 88 ESLint rules on a stock typescript-eslint config, and it ships equivalents of both `eslint-plugin-react-hooks` rules. Biome's guide says the migrated `biome.json` enables only the rules it could map, so diff it against your ESLint config before migrating.

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. It holds up, but it's 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.

What Do the Benchmark Numbers Show?

I ran both toolchains against the same 40 TypeScript files (13,810 lines), copied from this monorepo's shared library, on an Apple M1 Mac with Node 22.23.2. ESLint was 9.39.5 with the @eslint/js and typescript-eslint recommended presets (no type-aware rules), Prettier was 3.9.9 with two options set, and Biome was 2.5.14 running biome check, which lints and formats in one pass. Every figure is the median of seven runs, calling the binaries directly so npx startup doesn't pollute the timing.

One caveat before the table: ESLint 10.0.0 shipped on 2026-02-06 and 10.12.0 is the current release, so the ESLint column below is the 9.x maintenance line, not today's major. The setup carries over. ESLint 10 reads only flat config (eslint.config.*), which is the format I used, and the latest typescript-eslint (8.71.1) and eslint-plugin-react-hooks (7.1.1) both list ^10.0.0 in their ESLint peer ranges. I haven't timed ESLint 10, so I won't claim the gap moved either way.

ESLint 9.39.5 + Prettier 3.9.9Biome 2.5.14
Lint + format, cold (no cache files)1.86s0.20s
Lint + format, second run (--cache on both ESLint and Prettier)1.28s0.20s
Config files needed2 (eslint.config.js, .prettierrc)1 (biome.json)
node_modules on disk56.5MB56.4MB
Packages in npm ls --all --parseable1103

The gap is about 9x cold and 6x on the second run. That's a real difference, and it's smaller than the double-digit multiples you'll see quoted. I only measured this one size, so I wouldn't extrapolate it to a 2,000-file monorepo without running it there. Cold cache means no .eslintcache and no Prettier cache on disk, which is what most CI runners start from on every job. Note that Prettier's own cache barely helped: its check went from 0.82s to 0.88s between runs, so the warm-run cost is mostly Prettier.

The install-size row surprised me. I'd assumed the Rust binary would be the small side. On macOS arm64 it isn't: the platform binary alone accounts for nearly all of Biome's 56MB, roughly what ESLint, typescript-eslint, Prettier and the TypeScript package add up to (du -sk on both installs). What differs is the package count, 110 against 3, and fewer packages means fewer version-compatibility pins to keep patched over time.

Install size turned out to be a wash at about 56MB each way; the difference is 110 packages to keep compatible against 3, and that is where a single-binary linter earns its keep.

Share this Post on X Bluesky

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 ESLint config (Biome's guide says it handles both legacy and flat files and follows extends) 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.

Laptop and ultrawide monitor showing dense source code on a desk with headphones and a plant
Photo by Joshua Aragon on Unsplash

Here's the order I'd follow, and the one I used on the 40-file sample:

  1. Run biome init first to get a baseline config before touching anything else.
  2. Run biome migrate eslint --write and diff the generated rule list against your original .eslintrc line by line.
  3. Run biome migrate prettier --write last, 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": {
      "preset": "recommended",
      "correctness": {
        "noUnusedVariables": "error"
      },
      "suspicious": {
        "noExplicitAny": "warn"
      }
    }
  },
  "formatter": {
    "enabled": true,
    "indentStyle": "space",
    "indentWidth": 2
  }
}

On 2.5.14, biome init writes "preset": "recommended". The older "recommended": true still lints, but biome migrate flags it as needing an update, so a config copied from an older tutorial works and nags. After biome migrate prettier --write, I ran biome format over files Prettier had just formatted, and it flagged 4 of the 40. All four were long union types, which Biome breaks one member per line where Prettier keeps them on a single line. Small, but it means a formatter swap produces a real diff on first run.

Where Does the Rule Gap Actually Show Up?

Biome's migration report is the fastest honest answer to the coverage question, so I ran it against the benchmark's ESLint config (js.configs.recommended plus typescript-eslint recommended). It printed 88 ESLint rules found and 74 migrated directly. Seven more map to Biome rules it calls "inspired", which --include-inspired switches on, and that's where the 81 rules (92%) reported as covered comes from. Rule coverage is the share of your active ESLint rules that have a functionally equivalent Biome rule, and it matters far more than raw speed once a migration starts, because a fast linter that silently drops seven enforced rules just moved the risk somewhere less visible.

Editor context menu with Find Problems highlighted over a block of test code
Photo by Daniil Komov on Unsplash

The remaining seven are listed as not yet implemented: no-delete-var, no-invalid-regexp, no-new-symbol, no-octal, no-unexpected-multiline, @typescript-eslint/no-unused-expressions and @typescript-eslint/triple-slash-reference. On a typical app that list is mostly trivia, but no-unused-expressions is the kind of rule people notice missing.

The one I expected to find missing was eslint-plugin-react-hooks. It isn't. I wrote a component with a stale useEffect dependency array and a conditional useState call, then ran both tools over it. ESLint (with eslint-plugin-react-hooks 7.1.1) reported react-hooks/exhaustive-deps on the missing step dependency and react-hooks/rules-of-hooks on the conditional call. Biome reported useExhaustiveDependencies on the same effect and useHookAtTopLevel on the same call, with no plugin installed. Both rules switch on when Biome sees react in the project, and biome explain useExhaustiveDependencies lists the React domain. Whether the two agree on every edge case, custom hooks or unusual dependency shapes for instance, I haven't tested. Run both on your own code before you trust the match.

The gaps that are left, in the order I'd check them:

  • The seven unimplemented rules above. Read the list against what your config actually enables.
  • Type-aware typescript-eslint rules. Biome does ship noFloatingPromises, noMisusedPromises and useAwaitThenable, but its docs still list all three in the experimental nursery group, and they belong to the types domain. Enabling that domain makes Biome scan the whole project and run its own type inference engine, which the docs say has a performance cost, so the speed table above doesn't cover it. The typescript-eslint rule no-unnecessary-condition has a Biome counterpart under a plural name, noUnnecessaryConditions (stable, added in 2.1.4). biome explain noUnnecessaryCondition and biome explain noUnnecessaryTypeAssertion both returned "Unrecognized option" on 2.5.14, and I found no docs page for a type-assertion rule, so no-unnecessary-type-assertion is the one I'd treat as a gap.
  • Custom in-house ESLint plugins. A project-specific plugin enforcing an internal convention has no automatic migration path. Biome does have its own plugin mechanism now: plugins are GritQL (.grit) files listed under plugins in biome.json, they target JavaScript, CSS and JSON, and since 2.5 they can attach code fixes. I haven't tried it. It's a pattern-matching language rather than ESLint's visitor API, so budget a rewrite of each custom rule, or keep ESLint for that one job.

None of these are hard blockers. They're specific things to check against your actual config, not the marketing page's rule-count comparison.

Biome's own migrate report mapped 81 of 88 ESLint rules on a stock typescript-eslint config, and it ships both eslint-plugin-react-hooks rules, so the real gap is a short unimplemented list, type-aware checks and your custom plugins.

Share this Post on X Bluesky

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:eslint": "eslint ."
  }
}

Here eslint.config.js shrinks to the rules that didn't make it into the generated biome.json, plus any custom plugin. It's not the cleanest long-term architecture, but it's a sensible migration path: take the formatting speed win immediately, take most of the linting speed win, and keep a narrow ESLint invocation for what Biome can't do yet rather than blocking the whole migration on 100 percent rule parity. I haven't run that hybrid long enough to tell you whether teams keep it or finish the cutover, so treat it as a first step rather than a verdict.

So Should You Switch to Biome?

Biome's speed is real and reproducible: about 9x cold on my sample, less once ESLint's cache is warm. Speed isn't the decision point, though. The decision is whether your specific rule set has Biome equivalents, and the tool will tell you in one command. Run biome migrate eslint, diff the biome.json it writes against your ESLint config (the guide says it sets the preset to none and enables only what it migrated, so whatever is missing is your gap), and decide from that 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.

Frequently Asked Questions

Is Biome faster than ESLint and Prettier?
Yes. On a 40-file, 13,810-line TypeScript sample, Biome 2.5.14 ran lint plus format in about 0.2 seconds, against about 1.9 seconds cold and 1.3 seconds with caches for ESLint 9.39.5 plus Prettier 3.9.9, so roughly 9x and 6x. That was the ESLint 9 line; ESLint 10.12.0 is current and I haven't re-timed it. Biome runs both jobs in one process instead of two separate JavaScript tools with their own parsing passes. The multiplier depends on file count and on whether your ESLint config uses type-aware rules, which I did not enable.
Does Biome support all the same rules as ESLint?
No, but the gap is smaller than the usual warning. On a stock ESLint 9 plus typescript-eslint recommended config (I haven't re-run it against ESLint 10, whose recommended set adds no-unassigned-vars, no-useless-assignment and preserve-caught-error), biome migrate eslint found 88 rules and reported 81 (92%) as covered, with 7 not yet implemented. Biome also ships equivalents of both eslint-plugin-react-hooks rules, useExhaustiveDependencies and useHookAtTopLevel. The real gaps are the unimplemented rules, typescript-eslint's heavier type-aware rules, and any custom in-house plugin. The migrated biome.json enables only the rules the command could map, per Biome's guide, so diff it against your ESLint config before switching.
Can I use Biome for formatting only and keep ESLint for linting?
Yes, and it's a common incremental adoption path. Biome's formatter alone is a drop-in Prettier replacement for most codebases, faster and with fewer configuration options by design (a deliberate opinionated choice, similar to Prettier's own philosophy). Running Biome for formatting and keeping ESLint for the rules Biome doesn't yet cover gets you most of the speed win without a full linter migration.