Skip to content

Biome vs ESLint: The 400 Lines of Config I Deleted

Biome vs ESLint: The 400 Lines of Config I Deleted

Confession: I had an eslint.config.js sitting in a client repo that I hadn’t read in about eleven months. I could explain maybe four of the rules in it. The rest showed up via extends, got argued about once in a code review sometime in 2024, and then just sat there. Every few weeks CI would fail on a rule nobody could defend, someone would drop an // eslint-disable-next-line on it, and we’d move on with our lives.

What finally pushed me to switch wasn’t speed. Everybody leads with speed and I think it’s the least interesting part. It was running npm ls one afternoon and counting eleven ESLint-adjacent devDependencies in a mid-size Next.js app, with a tree deep enough that Renovate was opening two or three PRs a week against tooling that produces zero user-facing value.

So I moved that repo to Biome. The formatting half went great. The linting half did not go great, and I want to be specific about why, because most of the posts I read on this were written by people who migrated a personal blog and declared victory.

What I was actually maintaining

Here’s the before. Four config files plus a chunk of package.json:

// eslint.config.js
import js from "@eslint/js";
import tseslint from "typescript-eslint";
import react from "eslint-plugin-react";
import reactHooks from "eslint-plugin-react-hooks";
import jsxA11y from "eslint-plugin-jsx-a11y";
import importX from "eslint-plugin-import-x";
import unicorn from "eslint-plugin-unicorn";
import next from "@next/eslint-plugin-next";
import prettier from "eslint-config-prettier";

export default tseslint.config(
  js.configs.recommended,
  ...tseslint.configs.recommendedTypeChecked,
  { languageOptions: { parserOptions: { projectService: true } } },
  react.configs.flat.recommended,
  { plugins: { "react-hooks": reactHooks }, rules: reactHooks.configs.recommended.rules },
  jsxA11y.flatConfigs.recommended,
  importX.flatConfigs.recommended,
  unicorn.configs["flat/recommended"],
  { plugins: { "@next/next": next }, rules: next.configs["core-web-vitals"].rules },
  prettier,
  { rules: { /* ~40 lines of local overrides */ } },
);
// .prettierrc.json
{ "semi": true, "singleQuote": false, "trailingComma": "all", "printWidth": 100 }

Plus .eslintignore, .prettierignore, and four npm scripts to run lint and format in both check and write mode. About 180 lines of configuration when you add it all up, and that’s before the overrides file I’m not showing you.

The after is one file:

// biome.json
{
  "$schema": "https://biomejs.dev/schemas/2.5.0/schema.json",
  "vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true },
  "formatter": { "indentStyle": "space", "indentWidth": 2, "lineWidth": 100 },
  "linter": {
    "rules": { "recommended": true },
    "domains": { "next": "recommended", "react": "recommended", "test": "recommended" }
  },
  "javascript": { "formatter": { "quoteStyle": "double", "semicolons": "always" } }
}

One devDependency. One binary. No parser, no plugin resolution, no eslint-config-prettier sitting at the bottom of the array to stop two tools from fighting over semicolons.

The migrate command does most of it, then hands you homework

Biome ships biome migrate eslint --write and biome migrate prettier --write, and they genuinely work. The migration guide is honest about the limits, which I appreciated: it needs Node to load your flat config, it can choke on plugins that export cyclic references, and it won’t read YAML configs.

Two things caught me out.

First, the migration sets "recommended": false and then lists every rule it could map explicitly. That’s the safe behavior, but you end up with a 200-line biome.json that’s a translation of your old mess rather than a fresh start. I ran the migration, read the diff to see which rules I’d actually been relying on, then threw the output away and hand-wrote the nine-line config above. Took twenty minutes and I understand every line of it now.

Second, by default Biome skips rules it considers “inspired by” rather than identical to the ESLint original. You need --include-inspired if you want those. I missed this on the first pass and wondered why three rules I cared about had vanished.

The other thing worth knowing: Biome defaults to tabs. If your team has opinions about that, set indentStyle before you run biome check --write across the repo, or your first commit is going to be 40,000 lines of nothing.

Prettier lost and I stopped caring

The formatter is the easy win. Biome’s formatter aims to match Prettier closely and publishes the places where it deliberately differs. Running it over the repo produced a diff I skimmed in ten minutes: some JSX attribute wrapping, a few comment placements, one template literal it indented differently. Nothing that changed behavior.

Speed is real, but not in the way benchmarks sell it. Formatting the whole repo went from roughly nine seconds to under one. That barely matters in CI. What matters is format-on-save in the editor, where Prettier had a perceptible lag on our larger files and Biome doesn’t. Small thing that I notice every single day.

The bigger win is one fewer tool in the argument. eslint-config-prettier exists purely because two tools both wanted to own whitespace. Deleting that whole category of problem felt like the same relief I got when I stopped fighting my test runner.

Where I kept ESLint anyway

Here’s the part that gets left out of the switch-today posts.

I could not fully drop ESLint from that Next.js repo. Three reasons.

eslint-config-next has rules that check things Biome has no equivalent for, like flagging a raw <img> where next/image belongs, or catching a synchronous script in the wrong place. Biome’s Next domain covers some overlap, but not the framework-specific stuff that only makes sense if you understand Next’s build output.

We had two in-house rules written as ESLint plugins, one enforcing a naming convention on our API route handlers and one banning direct imports from a legacy module. Biome supports plugins through GritQL, and I ported the import ban in about half an hour. The naming rule needed logic that GritQL’s pattern matching couldn’t express cleanly, so it stayed.

And the strict type-aware rules: no-unsafe-assignment, no-unsafe-member-access, strict-boolean-expressions. The ones that need the full type graph. Biome has a real answer here, which I’ll get to, but it isn’t parity yet.

So the honest end state on that repo: Biome formats and handles 90% of linting on every save and every commit. ESLint runs once, in CI, with a config trimmed to about fifteen rules and four plugins. That’s still a meaningful reduction, and my editor no longer stalls. But “I deleted ESLint” would be a lie, and I keep seeing people write it.

On two smaller projects with no Next.js and no custom rules, ESLint is genuinely gone. So the answer depends heavily on what you built.

Type inference without tsc is the actually-new idea

The thing that makes Biome more than a fast Prettier clone is that v2 shipped type-aware lint rules that don’t invoke the TypeScript compiler. It has its own inference engine, built with sponsorship from Vercel.

If you’ve ever waited on recommendedTypeChecked in a large repo you know why this matters. typescript-eslint has to build a full program. That’s the single slowest thing in most lint setups, and it’s why so many teams turn type-aware rules off and then wonder why floating promises keep reaching production.

Biome’s approach trades completeness for speed. Their own numbers on noFloatingPromises put it at roughly 85% of the cases typescript-eslint catches, at a fraction of the cost. I’ve been running it for a few months and that feels about right. It catches the obvious async call with no await. It gets confused by promises that travel through a couple of layers of generic wrappers.

I keep going back and forth on whether 85% at 10x the speed is the better trade than 100% that people disable. For a rule like this one I think it clearly is, because the failure mode of the fast version is a missed warning and the failure mode of the slow version is nobody running it at all. For something like no-unsafe-assignment, where partial coverage gives you false confidence about untyped data crossing a boundary, I’m less sure.

What’s still missing in mid-2026

Worth knowing before you plan a migration around it. Biome doesn’t parse Markdown at all, and the 2026 roadmap says that’s blocked on finding someone to champion the work. If Prettier is formatting your docs and MDX, it stays.

YAML formatting isn’t shipped yet, though the parser is close. SCSS support started recently. Vue, Svelte, and Astro have what the team calls experimental full support, and they’ve publicly acknowledged that the Svelte announcement got ahead of the actual state of things, which honestly made me trust the rest of their claims more.

The team also flagged their own monorepo handling as a mistake. Auto-discovering nested configs sounded nice and produced memory leaks in large repos. The plan is an opt-in workspaces field instead. If you’re running a big monorepo, read that section before you commit. I hit a related version of this with package manager behavior when I tracked down a phantom dependency last year, and nested tooling config in monorepos is reliably where the sharp edges live.

Also: ESLint’s flat config is a real improvement over eslintrc, and oxlint is moving fast in the same Rust-based direction. This isn’t a settled question. Biome has around 15 million monthly npm downloads and 500 lint rules as of v2.5, which is enough that I’m comfortable betting on it, but I’d revisit in a year.

Try this on a small repo first

Pick your least important TypeScript project. Run these:

npm i -D --save-exact @biomejs/biome
npx biome migrate eslint --write --include-inspired
npx biome migrate prettier --write
npx biome check --write .

Read the diff. Not the summary, the actual diff. You’ll learn more about what your old config was doing in fifteen minutes than in a year of it silently passing.

Then make the real decision, which isn’t “Biome or ESLint.” It’s: what’s the smallest set of ESLint rules I can’t get elsewhere, and is keeping ESLint around for those worth the dependency tree? On my Next.js repo the answer was yes, barely. On everything else it was no.

If you want to see how I wire this into CI alongside the rest of a build, I keep notes on that sort of thing in my project write-ups.