Skip to content

Biome vs ESLint in 2026: What I Switched, What I Kept

Biome vs ESLint in 2026: What I Switched, What I Kept

Confession: I tried to throw ESLint out twice last year and crawled back both times. The third try stuck, and only for some of my projects. So if you’re reading yet another “Biome replaces ESLint” piece and feeling skeptical, good. I was too.

This is what actually happened when I ran Biome on a Next.js app, a small Node CLI, and a chunky monorepo with about 180k lines of TypeScript. I kept notes. Some of them are not flattering to Biome. Some are not flattering to ESLint. I’ll get to both.

What Biome got right out of the box

The install is silly. One package, one binary, one biome.json. No @typescript-eslint/parser, no eslint-config-this, no eslint-plugin-that. I deleted 14 dev dependencies on the Next.js project and the lockfile shrank by about 8 MB.

The speed is the other obvious thing. On the monorepo I went from a 41-second ESLint run (with cache cold) to about 1.4 seconds with Biome. Even with the ESLint cache warm it was 6+ seconds. Biome stays under two seconds basically forever because it’s Rust and it parallelizes by default. I stopped piping lint output through tee because there was no point. It’s done before I can read the first line.

The formatter is the unexpected win. I expected Biome’s formatter to be “Prettier, but in Rust.” It mostly is, but the defaults are saner. quoteStyle: "double", trailingCommas: "all", semicolons on. The diff against a Prettier-formatted codebase was small enough that I just accepted it and committed. The team noticed nothing.

If you want the short version of the config, this is what a working Next.js Biome config looks like:

{
  "$schema": "https://biomejs.dev/schemas/2.0.0/schema.json",
  "linter": {
    "enabled": true,
    "rules": { "recommended": true }
  },
  "formatter": {
    "enabled": true,
    "indentStyle": "space",
    "indentWidth": 2,
    "lineWidth": 100
  },
  "javascript": {
    "formatter": { "quoteStyle": "double", "trailingCommas": "all" }
  }
}

Compare that to the ESLint v9 flat config I was carrying around:

import js from "@eslint/js";
import ts from "typescript-eslint";
import next from "@next/eslint-plugin-next";
import react from "eslint-plugin-react";
import hooks from "eslint-plugin-react-hooks";
import a11y from "eslint-plugin-jsx-a11y";
import unused from "eslint-plugin-unused-imports";
import prettier from "eslint-config-prettier";

export default ts.config(
  js.configs.recommended,
  ...ts.configs.recommendedTypeChecked,
  { plugins: { next, react, "react-hooks": hooks, "jsx-a11y": a11y, "unused-imports": unused } },
  { rules: { /* about 40 lines of overrides */ } },
  prettier,
);

That’s eight packages I’m not maintaining anymore. Worth it on its own.

Where I’m still keeping ESLint

Here’s the part the “just switch” posts skip. Biome does not have every rule ESLint has. Not even close. The recommended set in Biome 2.x is solid for catching the obvious stuff: noUnusedVariables, noExplicitAny, useExhaustiveDependencies for hooks. It does not have, last time I checked, equivalents for eslint-plugin-jsx-a11y’s deeper accessibility rules, eslint-plugin-import’s resolver rules, or most of the ecosystem rules I lean on (@tanstack/eslint-plugin-query, eslint-plugin-testing-library, eslint-plugin-tailwindcss).

For the Next.js app, none of that mattered enough to keep ESLint. The useExhaustiveDependencies rule in Biome catches the dependency-array mistakes that used to bite me, and that was the rule I cared about most.

For the monorepo, it did matter. We have an eslint-plugin-internal package with about 30 custom rules. Things like “don’t import from apps/* inside packages/*,” “do not use the legacy formatCurrency helper,” and the company-specific conventions every monorepo accumulates. Biome’s GritQL plugin story is improving but it’s not where ESLint has been for years. So on that repo I left ESLint running with just the custom plugin and our internal rules, and let Biome do the lint and format pass for everything else.

That hybrid setup is not as ugly as it sounds. The ESLint config went from 600 lines to about 40, because all the formatting rules and most of the generic linting got handed off. Biome runs in the pre-commit hook. ESLint runs in CI as a second pass for the internal rules. Lockfile went from chunky to less chunky, runtime got cut, and I still get the rules I wrote myself.

For a side-by-side on how I think about “reach for X vs Y” tradeoffs in test tooling, I went through similar territory in my Vitest vs Jest comparison. Same flavor of decision, different tool.

The migration potholes

A few things I wish someone had told me before I ran the migration script. The official biome migrate eslint and biome migrate prettier commands work, and they read your existing config and produce a starter biome.json. They are not magic. Specifically:

  1. Custom rules are silently dropped. If you had "my-internal-plugin/no-foo": "error" in ESLint, the migration tool ignores it. No warning. You have to grep your old config for anything that didn’t migrate and decide what to do.
  2. Severity differences. ESLint’s “warn” maps to Biome “warn”, but some rules that were “error” in your ESLint config might land on Biome’s “info” tier. I had a few unused-imports issues sneak past my pre-commit hook because of this. Fix is to set the severity explicitly in biome.json after migration.
  3. Glob ignore differs. Biome uses its own files.includes array and it does not honor .eslintignore. If you had a clever .eslintignore that excluded generated files, port it over manually. Otherwise Biome will happily try to lint your built dist/ directory and complain about minified code, which is its own kind of comedy.
  4. Editor extension caching. If your team is on Cursor or Windsurf, double-check the Biome extension actually installs. Most of the time it does, but I’ve had two engineers hit a stale extension cache that I never fully diagnosed.

The Biome team has been public about plugin compatibility being a roadmap item, but “eventually” is doing a lot of work in that sentence. If your toolchain is built on five custom plugins, plan to keep ESLint around in some form.

What the speed actually buys you

I was skeptical that lint speed mattered. ESLint’s 41-second run was annoying but not the bottleneck of my day. Then I noticed something. With ESLint that slow, I had implicitly trained myself to only lint on save, and to commit fast and let CI catch the real problems. With Biome, the lint runs on every file change without being noticeable, the editor underlines the bad line before I’ve finished typing the next one, and I almost never push code that fails CI lint anymore.

It’s a behavioral change I wasn’t expecting. The faster the feedback loop, the more I actually use the tool. That’s true for TypeScript’s incremental type-checking, it’s true for tests, and now it’s true for lint. I knew this intellectually. I kept underestimating it in practice.

When I’d recommend switching

Here’s my actual decision tree, after six months.

Switch to Biome fully if your repo is a typical Next.js or Vite or Node app with mostly off-the-shelf ESLint plugins, no custom rules, and you’d be happy with the recommended ruleset plus a couple of overrides. The migration is an afternoon. The maintenance saving is real.

Run Biome alongside ESLint if you have custom internal rules, a deep accessibility audit setup, or you rely on eslint-plugin-import for cycle detection on a large codebase. The hybrid is fine. Don’t let perfect be the enemy of “my lockfile is now 8 MB smaller.”

Stay on ESLint if you have a complex plugin ecosystem you don’t want to rebuild, you’re on a legacy codebase where the ESLint config is load-bearing, or your team has muscle memory you can’t justify retraining. ESLint v9’s flat config is fine. It’s not slow because it’s bad, it’s slow because it’s doing a lot.

This is the kind of “depends on the project” answer I usually give in my client work. There is rarely one tool to rule them all, and the cost of switching is paid by humans, not benchmarks.

What to do this week if you’re curious

Pick the smallest repo you maintain. Run npx @biomejs/biome init in it, then npx @biomejs/biome check --write .. Look at the diff. If it’s small, commit it and try the editor extension for a week. If it’s a hundred files of formatting churn, you have a Prettier-vs-Biome config gap to settle first, and I’d resolve that before touching the linter.

The install is reversible. The mental model isn’t. Once you’ve had sub-second lint feedback you don’t really go back to multi-second runs without grumbling. Consider yourself warned.