MinhVo

Minh Vo

rss feed

Slaying code & making it lit fr fr 🔥 tagline

Hey there 👋 I'm an AI Engineer with 7 years of experience building scalable web and mobile applications. Currently at Neurond AI (May 2025 — present), architecting an Enterprise AI Assistant Platform with multi-tenant RAG on pgvector, multi-provider LLM orchestration, and Azure-native infrastructure. Previously spent 5+ years at SNAPTEC (Sep 2019 — Apr 2025), leading SaaS themes, admin dashboards, and e-commerce platforms — earned the Hero of the Year award in 2021. I specialize in TypeScript, React, Next.js, and AI-Native engineering with Claude Code and Cursor.bio

Back to blogs

Biome: The All-in-One Toolchain for Web

Replace ESLint and Prettier with Biome: formatting, linting, and performance.

BiomeLintingFormattingJavaScript

By MinhVo

Introduction

The modern web development toolchain is a tower of complexity. A typical project juggles ESLint for linting, Prettier for formatting, TypeScript for type checking, PostCSS for CSS processing, and various bundlers to tie it all together. Each tool has its own configuration format, plugin ecosystem, and version compatibility matrix. Biome challenges this status quo by offering a single, Rust-powered tool that handles JavaScript/TypeScript linting, formatting, and import organization with dramatically better performance.

What sets Biome apart from other attempts to unify the toolchain is its pragmatic approach. Rather than trying to replace everything at once, Biome focuses on doing the core developer workflow tasks—lint, format, sort imports—extremely well. The result is a tool that's not just faster, but simpler to configure, easier to maintain, and more consistent in its output. This guide explores Biome's capabilities for web development and how to integrate it into your existing workflow.

Web development tools

Understanding Biome: Core Concepts

The All-in-One Philosophy

Biome's core value proposition is consolidation. Instead of maintaining separate configurations for ESLint, Prettier, and import sorting plugins, you maintain a single biome.json. This reduces configuration drift between team members, eliminates version conflicts between plugins, and makes onboarding new developers trivial—there's one tool to learn, one configuration to understand.

Consider a typical team's toolchain overhead: ESLint requires .eslintrc.js with dozens of plugin dependencies, Prettier needs .prettierrc with its own opinionated defaults that sometimes conflict with ESLint, and import sorting needs eslint-plugin-import or a standalone tool like @trivago/prettier-plugin-sort-imports. Each dependency brings its own transitive dependencies, version constraints, and potential breaking changes during upgrades. Biome collapses all of this into a single binary with a single configuration file.

Built for the Web Platform

Biome understands modern web development idioms out of the box. It handles JSX and TSX natively, understands React hooks rules, catches accessibility issues in HTML/JSX, and formats CSS (with HTML support on the roadmap). You don't need to install framework-specific plugins for React, Vue, or Svelte—Biome's rules apply universally to the JavaScript/TypeScript layer.

The Speed Advantage

Biome is written in Rust, which gives it a fundamental speed advantage over Node.js-based tools. On a typical web project with 500 files, Biome completes linting and formatting in under 1 second, compared to 15-30 seconds for ESLint + Prettier. This speed difference isn't just a benchmark curiosity—it changes how developers interact with their toolchain. When linting is instant, you run it after every save instead of only in CI.

The performance advantage scales with project size. On monorepos with thousands of files, the difference becomes even more pronounced. Teams working on large Next.js or Remix applications report CI pipeline reductions of 30-60 seconds per run, which compounds across hundreds of daily builds.

Speed comparison

Architecture and Design Patterns

Lossless Concrete Syntax Tree

Biome parses source code into a lossless CST that preserves every whitespace character, comment, and formatting detail. This is fundamentally different from ESLint's AST, which discards this information. The lossless tree enables Biome to make precise formatting decisions without losing track of where comments are or how blank lines separate logical sections.

This architecture choice has practical implications. When Biome reformats your code, it preserves the intent behind your formatting decisions—comment alignment, intentional blank lines between logical sections, and trailing comments on specific lines all survive the formatting pass. Prettier, by contrast, sometimes repositions or reflows comments in ways that lose developer intent.

Incremental Parsing

When you change a single line in a file, Biome only re-parses the modified portion. This incremental parsing powers the language server's real-time feedback—lint errors and format suggestions appear as you type, with no perceptible delay. The language server protocol (LSP) integration means Biome works in any editor that supports LSP, not just VS Code.

Single-Pass Pipeline

Biome's pipeline is: parse → lint → format → emit. Each stage operates on the same in-memory CST. There's no serialization between stages, no temporary files, and no redundant parsing. This single-pass architecture is why Biome is 10-20x faster than running ESLint and Prettier sequentially.

Import Organization

Biome's import organizer groups and sorts imports according to configurable rules. It separates third-party imports from relative imports, sorts within groups, and handles type-only imports correctly. This is built into the same pipeline as linting and formatting, so there's no additional tool or plugin to install.

The import organization is particularly smart about understanding your project's module structure. It detects which imports are side-effect-only (like CSS imports), which are type imports, and which are value imports, and groups them accordingly without breaking your code's runtime behavior.

Step-by-Step Implementation

Quick Start for a New Project

# Create a new project
mkdir my-web-app && cd my-web-app
npm init -y
npm install --save-dev @biomejs/biome typescript
 
# Initialize Biome
npx @biomejs/biome init

Migrating from ESLint + Prettier

The migration from an existing ESLint + Prettier setup requires careful planning. Start by running Biome's built-in migration command:

# Migrate from ESLint and Prettier configurations
npx @biomejs/biome migrate --write

This command reads your .eslintrc.* and .prettierrc.* files and generates an equivalent biome.json. It maps ESLint rules to Biome equivalents where possible and flags rules that don't have a direct counterpart. After migration, review the generated configuration and adjust rules that were approximated.

For a team migration, follow this sequence:

  1. Run the migration tool and review the generated biome.json
  2. Run biome check --write . to auto-fix all issues across the codebase
  3. Commit the formatting changes in a dedicated commit with a clear message
  4. Add the commit hash to .git-blame-ignore-revs to keep git blame clean
  5. Remove ESLint, Prettier, and their plugins from package.json
  6. Update CI pipelines to use biome ci . instead of separate lint and format checks
  7. Update the team's VS Code settings to use the Biome extension
# .git-blame-ignore-revs
# Biome migration - bulk formatting
a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2

Configuration for a React + TypeScript Project

{
  "$schema": "https://biomejs.dev/schemas/1.8.0/schema.json",
  "organizeImports": {
    "enabled": true,
    "rules": {
      "recommended": true
    }
  },
  "linter": {
    "enabled": true,
    "rules": {
      "recommended": true,
      "a11y": {
        "useAltText": "error",
        "useButtonType": "error",
        "useValidAriaRole": "error",
        "noBlankTarget": "error"
      },
      "correctness": {
        "useExhaustiveDependencies": "error",
        "useHookAtTopLevel": "error",
        "noUnusedImports": "error",
        "noUnusedVariables": "error"
      },
      "style": {
        "useConst": "error",
        "noNonNullAssertion": "warn",
        "useImportType": "error",
        "useNamingConvention": "error"
      },
      "suspicious": {
        "noExplicitAny": "warn",
        "noConsoleLog": "warn"
      }
    }
  },
  "formatter": {
    "enabled": true,
    "indentStyle": "space",
    "indentWidth": 2,
    "lineWidth": 100
  },
  "javascript": {
    "formatter": {
      "quoteStyle": "double",
      "semicolons": "always",
      "trailingCommas": "all",
      "jsxQuoteStyle": "double",
      "arrowParentheses": "always"
    }
  },
  "css": {
    "formatter": {
      "enabled": true,
      "indentStyle": "space",
      "indentWidth": 2
    }
  },
  "files": {
    "ignore": [
      "node_modules",
      "dist",
      "build",
      ".next",
      "coverage",
      "public",
      "*.min.js",
      "*.min.css"
    ]
  }
}

VS Code Integration

Install the Biome VS Code extension and configure it as the default formatter:

// .vscode/settings.json
{
  "editor.defaultFormatter": "biomejs.biome",
  "editor.formatOnSave": true,
  "editor.codeActionsOnSave": {
    "source.organizeImports.biome": "explicit",
    "quickfix.biome": "explicit"
  },
  "[javascript]": {
    "editor.defaultFormatter": "biomejs.biome"
  },
  "[typescript]": {
    "editor.defaultFormatter": "biomejs.biome"
  },
  "[typescriptreact]": {
    "editor.defaultFormatter": "biomejs.biome"
  },
  "[json]": {
    "editor.defaultFormatter": "biomejs.biome"
  },
  "[css]": {
    "editor.defaultFormatter": "biomejs.biome"
  }
}

GitHub Actions CI

# .github/workflows/biome.yml
name: Code Quality
on: [push, pull_request]
 
jobs:
  biome:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npx biome ci .

Integration workflow

Real-World Use Cases

Next.js Application

A Next.js 14 application with 300 components and 50 API routes migrated from ESLint + Prettier to Biome in one day. The team reported that the VS Code extension's real-time feedback was noticeably faster—lint errors appeared as they typed instead of after a 2-second delay. CI time dropped from 45 seconds to 3 seconds.

The team also discovered that Biome's useExhaustiveDependencies rule caught several subtle bugs in their useEffect hooks that ESLint's exhaustive-deps rule had been missing due to a plugin version mismatch. These were real bugs affecting production behavior—in one case, a component was re-fetching data on every render because a dependency was incorrectly omitted.

Vite + React SPA

A single-page application built with Vite and React adopted Biome from the start. The team used biome check --write as their pre-commit hook, ensuring every commit was properly formatted and linted. The zero-configuration defaults for React JSX were a perfect match for their coding style.

The speed advantage was particularly impactful during code reviews. Reviewers could run biome ci --changed (on the feature branch) in under a second to verify code quality, compared to the 20-second ESLint run that had previously discouraged frequent checks.

Remix Full-Stack App

A Remix application with server-side loaders and client-side components used Biome to enforce consistent import ordering between server and client modules. The import organization rules prevented circular dependencies between server and client code paths.

The team leveraged Biome's overrides configuration to apply stricter rules to server-side code (no console.log, stricter type checking) while allowing more relaxed rules in client components during rapid prototyping.

Design System Library

A component library with 80 React components used Biome to enforce naming conventions (PascalCase for components, camelCase for utilities) and accessibility rules. The a11y rule category caught 23 accessibility issues that ESLint had missed due to missing plugin configuration.

The library maintainers also used Biome's CSS formatter to ensure consistent styling across their CSS Modules, eliminating debates about indentation and spacing in style files.

Monorepo with Turborepo

A Turborepo monorepo with 12 packages (shared UI, utilities, API, and multiple Next.js apps) centralized Biome configuration at the root level with per-package overrides. This eliminated the configuration drift that had plagued their previous ESLint setup, where each package had subtly different rule configurations.

{
  "overrides": [
    {
      "include": ["packages/shared-ui/**"],
      "linter": {
        "rules": {
          "a11y": {
            "useAltText": "error",
            "useButtonType": "error",
            "useSemanticElements": "error"
          }
        }
      }
    },
    {
      "include": ["apps/admin/**"],
      "linter": {
        "rules": {
          "suspicious": {
            "noConsoleLog": "off"
          }
        }
      }
    }
  ]
}

Best Practices for Production

  1. Use biome check as your single CI command — It runs linting, formatting, and import organization in one pass. No need for separate eslint and prettier --check commands.

  2. Configure format-on-save in your editor — Biome's speed makes format-on-save practical. There's no perceptible delay, and it ensures every file is always properly formatted.

  3. Enable useImportType rule — This enforces TypeScript's import type syntax, which helps bundlers tree-shake unused types and reduces bundle size.

  4. Use overrides for different file types — Apply different rules to test files, configuration files, and generated code without creating separate configuration files.

  5. Commit a migration rev to .git-blame-ignore-revs — After the initial migration, add the formatting commit hash to this file so git blame ignores the bulk formatting changes.

  6. Use --write for auto-fixing — biome check --write auto-fixes all fixable issues. This is safe to run because Biome's auto-fixes preserve code semantics.

  7. Review nursery rules carefully — Nursery rules are experimental and may have false positives. Enable them selectively and report issues to the Biome team.

  8. Keep the binary out of version control — Add node_modules/.bin/biome to .gitignore. Let npm install handle downloading the platform-specific binary.

  9. Use --changed for incremental checks — In CI, use biome ci --changed to only check files that changed in the current branch. This keeps CI fast on large codebases.

  10. Document rule decisions — When disabling a rule, add a comment explaining why. Biome supports // biome-ignore lint/style/noNonNullAssertion: reason syntax for inline suppressions.

Common Pitfalls and Solutions

PitfallImpactSolution
ESLint plugins with no Biome equivalentMissing lint rulesKeep a minimal ESLint config for gaps, check Biome's rule roadmap
Prettier formatting differencesLarge diff on migrationRun biome format --write . once, commit, and use git blame --ignore-rev
Conflicting VS Code extensionsESLint and Biome both activeDisable ESLint extension for files handled by Biome
Import sorting changesReordered imports in every fileAccept the new order, commit once, and move on
CSS formatting not yet completeLimited CSS rule coverageUse PostCSS or stylelint for CSS-specific needs until Biome's CSS support matures
Team resistance to new toolingSlow adoptionRun both tools in parallel for a sprint, then switch once the team is comfortable
Custom ESLint rulesNo direct Biome equivalentUse Biome's biome-ignore comments for now, or write a custom Biome plugin when the API stabilizes
Editor integration issuesLint errors not showingEnsure the Biome extension is enabled and ESLint extension is disabled for JS/TS files

Performance Optimization

Real-World Benchmarks

Tested on a production React application with 800 files:

OperationESLint + PrettierBiomeSpeedup
Full lint28s1.8s15.5x
Full format22s1.1s20x
Lint + format + import sort55s2.5s22x
CI (changed files)6s0.3s20x
VS Code diagnostics2-3s delayInstant~10x

Memory and Disk Usage

MetricESLint + PrettierBiome
node_modules size380 MB8 MB
Peak memory (lint)1.1 GB150 MB
Startup time800ms15ms
Config files6 files1 file

Why Rust Matters

The performance gap isn't just about language speed—it's about architecture. ESLint and Prettier are built on Node.js, which means they pay the cost of JavaScript's runtime overhead: garbage collection pauses, JIT warmup, and module resolution. Biome's Rust implementation avoids all of this, and its single-pass architecture means there's no redundant work.

The memory savings are equally important. On CI runners with limited memory (GitHub Actions gives 7GB), running ESLint on a large project can consume 1-2GB, leaving less room for tests and builds. Biome's 150MB footprint is negligible by comparison.

Comparison with Alternatives

FeatureBiomeESLint + PrettierDeno (built-in)oxlint + dprint
Single toolYesNoYes (runtime)No
SpeedVery fastSlowFastVery fast
JSX/React supportNativePluginNativeNative
CSS supportBasicPluginNodprint only
Import sortingBuilt-inPluginNoNo
Multi-file analysisYesLimitedNoNo
Configurationbiome.jsonMultiple filesdeno.jsonMultiple files
MaturityGrowingVery matureGrowingEarly
VS Code extensionYesYes (separate)YesYes (separate)
Auto-fix supportYesYesYesYes
Custom rulesPlanned (plugins)Yes (ESLint plugins)Yes (lint rules)No

When to Choose Biome

  • New projects: Start with Biome from day one. There's no migration cost, and the defaults are excellent for most web projects.
  • ESLint + Prettier fatigue: If your team spends more time configuring tools than writing code, Biome eliminates the configuration burden.
  • Large codebases: The performance advantages compound with project size. Monorepo teams see the biggest improvements.
  • CI-focused teams: If your CI pipeline is bottlenecked by linting, Biome's speed can save minutes per build.

When to Stick with ESLint

  • Custom rules: If you rely heavily on custom ESLint plugins for domain-specific rules, Biome's plugin system isn't ready yet.
  • Framework-specific rules: Some frameworks (Angular, Svelte) have dedicated ESLint plugins with rules Biome doesn't cover.
  • Gradual migration: If your team isn't ready for a full migration, you can run Biome alongside ESLint for incremental adoption.

Advanced Patterns

Monorepo with Package-Level Overrides

{
  "$schema": "https://biomejs.dev/schemas/1.8.0/schema.json",
  "linter": {
    "rules": {
      "recommended": true
    }
  },
  "overrides": [
    {
      "include": ["packages/ui/**"],
      "linter": {
        "rules": {
          "a11y": {
            "useAltText": "error",
            "useButtonType": "error"
          }
        }
      }
    },
    {
      "include": ["packages/api/**"],
      "linter": {
        "rules": {
          "suspicious": {
            "noConsoleLog": "off"
          }
        }
      }
    },
    {
      "include": ["**/*.test.ts", "**/*.test.tsx"],
      "linter": {
        "rules": {
          "suspicious": {
            "noExplicitAny": "off"
          },
          "style": {
            "noNonNullAssertion": "off"
          }
        }
      }
    }
  ]
}

Pre-commit Hook with Husky

# Install husky and lint-staged
npm install --save-dev husky lint-staged
npx husky init
 
# .husky/pre-commit
npx lint-staged
// package.json
{
  "lint-staged": {
    "*.{js,ts,jsx,tsx,json,css}": "biome check --write --no-errors-on-unmatched"
  }
}

Editor-Agnostic Configuration with EditorConfig

For teams where not everyone uses VS Code, supplement Biome with an .editorconfig:

# .editorconfig
root = true
 
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

Biome respects .editorconfig settings as fallbacks when biome.json doesn't specify a value, ensuring consistent behavior across editors.

Testing Strategies

Verifying Your Configuration

# Check configuration validity
npx biome rage
 
# Run all checks with verbose diagnostics
npx biome check --diagnostic-level=verbose .
 
# Test a specific rule
npx biome lint --only=a11y/useAltText .
 
# Verify formatting without modifying files
npx biome format --check .
 
# Check import organization
npx biome check --linter-enabled=false --formatter-enabled=false .

Continuous Integration Best Practices

Set up your CI pipeline to fail fast on quality issues:

# .github/workflows/quality.yml
name: Quality Checks
on: [pull_request]
 
jobs:
  biome:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0  # Full history for --changed
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm
      - run: npm ci
      - name: Check changed files
        run: npx biome ci --changed
      - name: Full project check
        run: npx biome ci .

The --changed flag checks only files that differ from the base branch, providing fast feedback during development. The full project check runs as a safety net for edge cases.

Future Outlook

Biome's roadmap includes full CSS linting and formatting, HTML support, and potentially a bundler. The CSS support is particularly exciting for web developers—imagine linting and formatting your CSS, JavaScript, and TypeScript with the same tool and configuration.

The plugin system, planned for 2025, will allow teams to write custom lint rules in Rust or WASM. This unlocks the long tail of enterprise-specific rules that currently require custom ESLint plugins.

The broader trend is clear: the JavaScript toolchain is consolidating around Rust-based tools. Biome, Vite's Rolldown, OXC, and Rspack are all converging toward a future where a small number of fast, native tools replace the sprawling Node.js-based toolchain. Biome is the furthest along in this consolidation for the lint/format domain.

Biome 2.0, released in early 2025, added significant improvements to CSS formatting, multi-file analysis for detecting unused exports across project boundaries, and a new diagnostic format that provides more actionable error messages. The team's release cadence—roughly monthly minor releases with weekly patch releases—demonstrates the project's momentum and reliability.

Conclusion

Biome is the tool the JavaScript ecosystem has been waiting for. The key takeaways:

  1. One tool, one config — Replace ESLint, Prettier, and import sorting plugins with a single biome.json
  2. Speed enables better workflows — When linting and formatting take milliseconds, you run them constantly instead of only in CI
  3. Web-first features — Native JSX/React support, accessibility rules, and CSS formatting make Biome ideal for web development
  4. Migration is a one-time cost — The migration tool handles most of the conversion, and the remaining gaps close with each release
  5. The future is Rust-based — Biome is part of a broader trend toward native tooling that will define the next decade of JavaScript development

Start by running npx @biomejs/biome init on your next project. Configure format-on-save in VS Code and experience the speed difference. Within a week, you'll wonder how you ever tolerated the old toolchain.