I Replaced ESLint and Prettier with Biome on a Client Project. Here's the Honest Report.

A tool comparison from a consulting engagement — what Biome actually delivered, where it fell short, and the decision I'd make again (and the one I wouldn't).


Two months ago a client asked me a simple question: "Can we make the pre-commit hook not take twelve seconds?" I spent a day digging in and the answer was, yes, but the thing slowing us down wasn't what they thought. It wasn't the type checker. It wasn't Jest. It was the lint-staged step running ESLint and Prettier over a handful of changed files.

Twelve seconds for maybe eight files. On a laptop with an M3 Pro. Something was off.

I ended up ripping both tools out and replacing them with Biome. The migration took a day and a half. Now the same hook finishes in under a second. But that speedup hides a more interesting story, and I want to write it down while it's fresh — partly because I've seen breathless "Biome is 25x faster" posts going around again and they don't quite match what I saw.

The project, briefly

The client runs a mid-sized TypeScript monolith: Next.js frontend, a NestJS backend, and a shared package of types and utilities. About 180k lines of TypeScript and TSX across the three workspaces. Eight engineers, a pretty standard setup: ESLint 8 with @typescript-eslint, eslint-plugin-react, eslint-plugin-react-hooks, eslint-plugin-import, eslint-plugin-jsx-a11y, plus Prettier for formatting. A .eslintrc.js that had grown organically over three years and nobody fully understood anymore.

The complaint was pre-commit slowness. The real problem, once I measured it, was that their ESLint config was loading 14 plugins on every run, and eslint-plugin-import alone was responsible for about 60% of the cold-start time because of how it resolves module paths.

What the migration actually looked like

Biome's migrate command handled most of the Prettier config automatically. That part was boring in the good way. The ESLint migration was less clean. Biome's converter picked up rules it supported and silently dropped the ones it didn't, which is fine until you realize you've lost coverage you cared about.

Here's what I had to deal with, rule by rule.

Rules Biome covered out of the box: roughly 70% of what we had. All the basic correctness stuff, the stylistic rules, the common TypeScript patterns. noExplicitAny, useExhaustiveDependencies (their version of react-hooks/exhaustive-deps), noUnusedVariables, useConst. These worked, and their error messages were honestly better than ESLint's in a few cases.

Rules I had to rewrite or give up: about 20%. Some were project-specific rules the previous team had built — one enforced a convention that NestJS service methods had to return Promise<Result<T>>. That had been a custom ESLint rule. Biome doesn't support custom rules the way ESLint does, so I moved that check into a small pre-commit script that greps the service files. Not elegant. Works.

Rules I couldn't replace at all: the remaining 10%. The big one was eslint-plugin-jsx-a11y. The client has accessibility requirements from a government contract, and Biome's a11y rules cover maybe half of what jsx-a11y does. I ended up keeping ESLint in the repo just for accessibility, running it only on .tsx files, only in CI, never locally. Ugly but honest.

Warning

If you have a custom ESLint plugin or you rely heavily on a framework-specific plugin like eslint-plugin-jsx-a11y or eslint-plugin-next, do a rule-by-rule audit before you migrate. Don't trust the converter's summary — it won't tell you what it dropped.

The speed numbers, measured honestly

I ran each tool on the same machine, on a warm filesystem, three times, and took the median. This is what I saw on their codebase:

ESLint (full repo, 14 plugins):   41.2s
Prettier (full repo, --check):     7.8s
Biome check (full repo):           1.9s

So not 25x. More like 25x on the frontend workspace only, which is small and React-heavy, and closer to 20x on the whole monorepo. Still a huge win. But I want to be careful with the framing — most of the delta isn't "Rust is fast." A chunk of it is "Biome doesn't have to load a plugin graph and walk TypeScript's type information every time."

The part that changed developer behavior wasn't the cold-start time. It was that lint-on-save in VS Code finally became invisible. Before the migration, engineers had the ESLint extension disabled because it was introducing visible latency when editing large files. After the migration, they left the Biome extension on, which meant they saw errors immediately instead of at commit time. That's the win I didn't predict.

Where I got bitten

Two things caught me out.

The first was that Biome's formatting is not byte-for-byte identical to Prettier. It's close, but there are edge cases — long union types in TypeScript, JSX prop wrapping past a certain width, trailing commas in function type parameters. The first PR after migration touched 1,400 files just from reformatting. I should have done that as a dedicated commit with a .git-blame-ignore-revs entry from day one. I didn't, and now git blame on some files points to me instead of the person who actually wrote the logic. Sorry, team.

The second was a subtler ordering issue with organizeImports. Biome sorts imports differently than eslint-plugin-import did. Harmless functionally, but it produced a noisy diff in every PR for the first week until people's local caches caught up.

Would I do it again?

For this client, yes. For every client, no. If their ESLint config had been full of custom rules or heavily tied to a framework plugin Biome doesn't support, I would have left it alone and spent that day and a half doing something else. The tool is good. It is not magic. The honest pitch is: if 90% of your lint config is common rules, Biome is strictly better. If you're in the other 10%, stay put until v3.

One thing I'm still turning over: the custom rule I dropped into a grep script — should that live in the repo forever, or should I wait until Biome ships a plugin system and migrate it back? I don't know yet. How long are you willing to run a script you know is a workaround before you call it the permanent solution?