Engineering

We Replaced ESLint with Biome and Deleted 40% of Our Config Files

Migrating a React Native + Next.js + Convex monorepo from ESLint to Biome. Faster linting, fewer config files, and one less reason to yell at CI.

By Lumo EngineeringFeb 7, 20262 min read

Our ESLint setup had 4 config files, 23 plugins, and took 12 seconds to lint the mobile app. Biome does it in under a second with zero plugins. This is the story of a migration that was boring in the best possible way.

The ESLint Tax

Every JavaScript developer has been here: you set up ESLint, it works great, then six months later you're maintaining a .eslintrc.js that's longer than some of your components. Plugins for React, React Native, TypeScript, import sorting, accessibility — each one adding its own configuration surface and its own opinions about whether your useEffect dependency array is complete.

In a monorepo, this multiplies. We had:

  • apps/mobile/.eslintrc.js — React Native rules
  • apps/web/.eslintrc.js — Next.js rules
  • packages/backend/.eslintrc.js — Convex/Node rules
  • Root .eslintrc.js — shared base config

Four configs, all slightly different, all occasionally conflicting, all slow.

Why Biome

Biome is a single binary that does linting and formatting. No plugins. No config inheritance chains. No peerDependency hell. It's written in Rust, which means it's fast enough that you stop thinking about speed entirely.

The migration was PR #2, and it was surprisingly mechanical:

  1. Remove every ESLint and Prettier package from package.json
  2. Delete every .eslintrc.* and .prettierrc.* file
  3. Add one biome.json at the root
  4. Run biome check --write . and fix the handful of legitimate issues it found
  5. Update CI

The one biome.json now covers the entire monorepo:

{
  "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
  "organizeImports": { "enabled": true },
  "linter": {
    "enabled": true,
    "rules": { "recommended": true }
  },
  "formatter": {
    "enabled": true,
    "indentStyle": "tab"
  }
}

That's it. That's the entire linting and formatting config for a monorepo with three apps and a shared backend.

The Speed Difference

Before (ESLint + Prettier):

  • Mobile app lint: ~12 seconds
  • Web app lint: ~8 seconds
  • Full monorepo: ~25 seconds

After (Biome):

  • Full monorepo: ~800ms

Not a typo. Sub-second for the entire codebase. CI pipelines that used to block on linting now finish before you can switch tabs.

What We Lost (And Why It's Fine)

Biome doesn't have the ESLint ecosystem. No eslint-plugin-react-hooks to yell at you about dependency arrays. No eslint-plugin-jsx-a11y for accessibility warnings. We accepted this trade-off because:

  1. TypeScript catches most of what ESLint plugins catch, just differently
  2. We'd rather have fast, consistent linting than comprehensive, slow linting
  3. The rules we actually enforced (import ordering, formatting, basic correctness) Biome handles natively

In the same PR, we also fixed the React Native build pipeline — adding build:android, build:ios, and proper Turborepo task configuration. The kind of foundational work that's invisible until it breaks.

The Takeaway

Developer tooling migrations are unsexy work. Nobody's writing a conference talk about switching linters. But the compound effect of 800ms lint times vs 25-second lint times, across every save, every commit, every CI run, for every developer — that's hundreds of hours over the life of a project.

Sometimes the best engineering decision is deleting config files.


Abdul Rafay Founder, Syntax Lab Technology

Loading...