Home / Articles / Replacing ESLint and Prettier with Biome: Speed vs the Hooks Rule You Still Need

This article is published in English.

Replacing ESLint and Prettier with Biome: Speed vs the Hooks Rule You Still Need

Lab timings for Biome versus ESLint and Prettier, the react-hooks/exhaustive-deps gap, and when dual-running one ESLint plugin is the honest migration.

1944 words

Marketing posts sell a Rust binary, a single config, and 20–50× gains. A four-route lab was timed afterward, then the diagnostics Biome would not carry were counted.

One binary versus stacks of plugins. Wall time dropped. Coverage for one important rule did not.

Inspect package.json.

Tally the lint-related dependencies. Trees that still ship eslint, prettier, eslint-config-prettier, eslint-plugin-react-hooks, plus a parser plugin, pay a quiet tax on every push. Biome markets itself as one executable for formatting, linting, and import sorting.

Biome was added beside the existing ESLint toolchain on that lab. Both pipelines were timed. ESLint was then removed. A depended-on diagnostic had no Biome counterpart that felt safe. The pipeline went green, and the bug class that diagnostic used to block returned on a feature branch.

The branch reattached a websocket when a theme switch flipped — a classic effect-dependency footgun. Biome stayed silent.

The sentence Biome actually publishes

Officially, Biome formats with about 97% Prettier agreement and lints with a large rule set inspired by ESLint and typescript-eslint. Settings live in biome.json. Day-to-day invocation looks like pnpm exec biome check --write.

What splashy posts omit: obscure plugins and house rules may still be absent. Either keep ESLint for those gaps or port the logic.

Speed is easy to love. Missing coverage is the rent.

The times actually recorded

The lab mixed TypeScript, React, and a handful of server modules — roughly sixty non-node_modules files. Instant Navigations stayed off so only the checker was measured.

pnpm exec eslint . --max-warnings=0
pnpm exec prettier --check .
pnpm exec biome check .

Three warm passes each; medians:

  • ESLint alone: 4.1s
  • Prettier --check: 1.6s
  • ESLint then Prettier: 5.7s
  • Biome check: 0.22s

Nowhere near 50×. Versus the sequential pair it is roughly 26× on a small tree. Blog benches often cite ~10k files. This clock used the app that actually deploys. Trend agrees; the splashy multiplier does not.

A fresh checkout versus Prettier disagreed on two files — a long JSX prop wrap inside InvoiceRow. Biome’s wrap was kept. The 97% figure is honest; the leftover 3% only becomes drama if a team argues file-by-file.

How to see it on your machine

pnpm add -D --save-exact @biomejs/biome
pnpm exec biome init

A biome.json appears. Run Biome once while ESLint still exists. Do not tear anything out until there is a written inventory of ESLint hits Biome never reports.

pnpm exec eslint . -f unix > /tmp/eslint.txt
pnpm exec biome check --reporter=json > /tmp/biome.json

Hand comparison showed most of recommended overlapping. The painful gap:

react-hooks/exhaustive-deps

Biome includes hooks-related checks. They did not match the exact warning previously tuned around a socket effect. After CI dropped that rule, a theme-switch reconnect slipped through.

ESLint returned for a single plugin:

{
  "scripts": {
    "lint": "biome check . && eslint app --plugin react-hooks --rule 'react-hooks/exhaustive-deps:error'"
  }
}

Clumsy. Transparent. Running two tools is a fair modern compromise when the missing rule has a name. Keeping a full ESLint tree “just in case” is usually waste.

Validate the editor story the same evening. Biome’s language server replaced a pair of extensions. Suddenly the hooks warning appeared only in CI — worse locally unless the ESLint extension remains for that one rule. Prefer seeing it on /settings over discovering it in the morning build.

What is actually faster

Biome walks the tree as one native process. ESLint is Node plus plugins, often followed by Prettier. At sixty files, loading plugins eats much of the 4.1s. At thousands of files the walk dominates and marketing-scale ratios appear.

Biome 2 offers some type-aware linting, but it is not the full typescript-eslint surface. If continuous integration still enables parserOptions.project, measure that job by itself — that is the expensive ESLint mode. Expect gaps versus older no-unsafe-* selections.

Turning off project-aware parsing, watching CI fall from ~90s to ~8s, and blaming Biome for the win is misleading when the type-aware pass was simply removed. Say that aloud when it happens.

The bill, itemised

Pipeline time: 5.7s down to 0.22s here. Monorepos feel the win hardest. Small apps mainly get a snappier laptop loop.

Rules: one depended-on check disappeared; ESLint stayed for that path.

Formatting fights: two JSX wraps; recoverable.

Editor: two extensions became one, then one returned — roughly even, with a faster CLI check.

Attention: a green Biome run is not a green hooks tree. Pull requests should note which runner flagged the file.

Leave it on, or stop the overnight shift

Greenfield repos can adopt Biome tonight for format plus baseline lint and skip Prettier entirely.

Legacy trees should dual-run for about a week. Retain ESLint only for named plugins. Delete unused config, not merely the CI step.

Remain on ESLint when custom rules are the product — and write those rules down. “Maybe we need plugins” is not an inventory.

Never drop exhaustive-deps purely because Biome is quick. A theme switch will demonstrate why the rule existed.

Caveats worth stating plainly

0.22s against 5.7s describes sixty files on one laptop — not a 10k-file corpus, not a 56× claim.

The hooks gap depends on config and Biome build that night. Re-run biome rage and inspect current hooks diagnostics before treating the hole as permanent; identifiers shift across releases.

Plugin inventories differ. If import-ordering via eslint-plugin-import is the sole holdout, trial Biome’s organize-imports for a week.

Clock both tools. Enumerate ESLint findings that remain after Biome is clean.

That enumeration is the migration work. Survivors feed a dual-lint script with clear ownership.

When CI went green and a socket reconnect returned

ESLint plus Prettier gave way to Biome. Sixty-file check time: 0.22s versus 5.7s. A branch merged. Flipping theme reattached a totals websocket. react-hooks/exhaustive-deps previously blocked that pattern; Biome did not emit the tuned twin.

Bad response. Park the socket until “migration finishes.” Customers lose the live figure.

Better response. Biome owns format and baseline lint. ESLint remains for one plugin under app.

{
  "scripts": {
    "lint": "biome check . && eslint app --plugin react-hooks --rule 'react-hooks/exhaustive-deps:error'"
  }
}

Two runners are honest when the absent rule is named. Two runners are wasteful when the whole ESLint config survives. Formatting deltas: two JSX wraps; Biome’s version kept.

Measure on your own tree. Catalog ESLint-only rules after Biome is clean. Catalog first; stopwatch second.

Lab session commands and outputs

Versions before the walk: Node 24, TypeScript 7, Next 16.3. Store them in a lab note so the next session is not guesswork.

node -v
pnpm exec tsc -v
pnpm exec next --version

Put those three lines at the top of the note. If a major mismatches the guide being followed, halt — later commands will mislead quietly.

Route walk next:

pnpm exec next dev

Hit /, /invoices, /invoices/1, /settings, then /invoices again. Leave DevTools preserve-log on. Capture the filter UI and the URL; that pair is useful across related experiments.

Typecheck:

pnpm exec tsc --noEmit --pretty false
echo $?

Exit code zero is not a shipped product. It only clears the path to runtime checks.

Afterward, rerun the discovery commands from “How to see it on your machine.” Do not skip them because numbers appear above. Another machine, ambient heat, or busy Chrome tabs will move memory, check duration, and abort timing more than a framework patch.

Keep a one-line failed-fix entry: “Tried X, still saw Y.” Versions, command, output, and that sentence make useful follow-up material. Marketing screenshots do not.

Failure diary

Removing ESLint the same day Biome arrived revived the socket issue. Restoring one plugin restored coverage.

Trying to mirror project-aware typescript-eslint behaviour in Biome 2 found partial overlap. The older no-unsafe-* cluster did not match. Pretending otherwise stopped.

A long JSX prop formatting conflict ended by accepting Biome.

In the editor, Biome’s LSP displaced Prettier and ESLint extensions. Hooks warnings then appeared only in CI, so the ESLint extension stayed for that rule. Two extensions still beat five.

Checklist before you delete ESLint

  • [ ] biome check finishes clean
  • [ ] ESLint-only rules are written down
  • [ ] Named plugins are kept or consciously abandoned
  • [ ] exhaustive-deps has a trusted twin, or ESLint stays for it
  • [ ] Format deltas are reviewed calmly, not panic-reverted
  • [ ] Both stopwatches were taken on the same tree

0.22s versus 5.7s is sixty files. Re-measure locally. Re-check hooks diagnostics on the installed Biome before quoting the gap as forever.

Production note from the invoices app

CI saved wall-clock seconds. During a demo, flipping theme revived the websocket reconnect because hooks coverage had vanished. Seconds are not the deliverable — the live aggregate is.

Prefer a slower lint that catches reconnects over a blazing check that misses them. Combining 0.22s Biome with one ESLint plugin is also a valid bargain. New repositories can begin on Biome alone and add ESLint once a rule is named. Older repositories gain nothing from purity theatre.

Commands

pnpm exec eslint . --max-warnings=0
pnpm exec prettier --check .
pnpm exec biome check .

Warm thrice; take medians. Log ESLint, Prettier, both, and Biome. Dump unix-format ESLint output and map findings Biome never emitted. Put the map in the pull request; treat the clock as a footnote.

The meaningful delta is the rule without a twin.

Reader exercise on a real tree

Choose a repository that ships, not a sandbox. Capture wall times for eslint ., prettier --check ., and biome check . — three warm runs each. Tabulate medians beside a file count.

Build the rule diff manually for a stretch; automated renames often mislead. List every ESLint finding that remains after Biome is clean. Empty list → ESLint can leave. List containing react-hooks/exhaustive-deps or a product-critical custom plugin → keep that slice.

Open a branch with a hook whose missing dependency is known to be real. See which tool complains. That branch is why the comparison is not merely a race.

Commit the table and the rule list. Avoid a “switched to Biome” pull request that only deletes packages. Deletion is the closing step, not the opening move.