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 2.0: The Fast JavaScript Toolchain

Biome 2.0: multi-file analysis, project-level linting, and improved formatter.

BiomeLintingFormattingJavaScript

By MinhVo

Introduction

The JavaScript toolchain has long been a fragmented landscape of competing tools. ESLint for linting, Prettier for formatting, various bundlers and transpilers—each requiring its own configuration, plugin ecosystem, and maintenance burden. Biome emerged from this chaos with a radical proposition: one tool, written in Rust, that handles linting, formatting, import organizing, and more at speeds that make traditional Node.js-based tools look glacial.

Biome 2.0 represents a significant leap forward, introducing multi-file analysis, project-level linting intelligence, and a vastly improved formatter that handles edge cases the original release struggled with. For teams drowning in configuration files and slow CI pipelines, Biome 2.0 offers a compelling path to simplification. This guide explores what makes Biome 2.0 tick, how to migrate from ESLint/Prettier, and the architectural decisions that make it so fast.

JavaScript tooling evolution

Understanding Biome: Core Concepts

Why Biome Exists

The JavaScript ecosystem suffers from toolchain fatigue. A typical project might have ESLint with 15+ plugins, Prettier with custom configuration, import sorting plugins, TypeScript type-checking, and various CI scripts to orchestrate all of them. Each tool parses the same source files independently, duplicating work and introducing inconsistencies.

Biome consolidates these tools into a single Rust-based binary. It parses source code once and runs linting, formatting, and import organization on the shared AST. This eliminates redundant parsing, reduces configuration surface area, and produces deterministic output—no more debates about whether Prettier or ESLint should format a particular construct.

The Architecture: Rust and the Rowan Parser

Biome is built in Rust using the Rowan library for lossless syntax trees. Unlike ESLint's ESTree-based AST, Biome's CST (Concrete Syntax Tree) preserves every whitespace, comment, and formatting detail. This enables precise formatting decisions and lint rules that can reason about code structure without losing track of where comments or blank lines appear.

The Rowan-based parser is incremental—when you change a single line in a file, Biome only re-parses the affected portions. This makes the language server (Biome's LSP implementation) exceptionally responsive, even in large monorepos.

Multi-File Analysis in 2.0

The headline feature of Biome 2.0 is multi-file analysis. Previous versions could only analyze files in isolation. Biome 2.0 builds a project graph that understands module relationships, enabling lint rules that consider how symbols are imported, exported, and used across files. This unlocks rules like detecting unused exports, verifying import paths, and enforcing module boundary constraints.

Biome architecture

Architecture and Design Patterns

Single-Pass Pipeline

Biome's core pipeline is a single-pass architecture: parse → lint → format → emit. Each stage operates on the same in-memory CST, avoiding the serialization overhead that plagues multi-tool pipelines. This is why Biome can lint and format a 10,000-file project in seconds where ESLint + Prettier might take minutes.

Rule Categories

Biome organizes lint rules into four categories:

  • Source rules enforce file-level conventions (e.g., no duplicate imports)
  • A11y rules enforce accessibility standards (e.g., alt text on images)
  • Correctness rules catch likely bugs (e.g., unused variables, unreachable code)
  • Style rules enforce formatting preferences (e.g., no unused template literals)
  • Nursery rules are experimental and may change between releases

Each rule can be set to error, warn, off, or info. Rules are grouped by category, making it easy to enable entire sets at once.

Configuration Cascade

Biome uses a cascading configuration system similar to ESLint. A root biome.json defines project-wide settings, and child biome.json files in subdirectories can override specific settings. This enables monorepo setups where different packages have different linting rules.

Step-by-Step Installation and Migration

Installing Biome 2.0

// package.json
{
  "devDependencies": {
    "@biomejs/biome": "2.0.0"
  },
  "scripts": {
    "lint": "biome lint .",
    "format": "biome format . --write",
    "check": "biome check .",
    "ci": "biome ci ."
  }
}

Migrating from ESLint and Prettier

Biome provides a built-in migration command that reads your existing ESLint and Prettier configurations and generates an equivalent biome.json.

# Migrate from ESLint and Prettier
npx @biomejs/biome migrate --write
 
# This generates biome.json based on your existing configs
# Review the output and adjust as needed

Configuring biome.json

// biome.json
{
  "$schema": "https://biomejs.dev/schemas/2.0.0/schema.json",
  "organizeImports": {
    "enabled": true
  },
  "linter": {
    "enabled": true,
    "rules": {
      "recommended": true,
      "correctness": {
        "noUnusedVariables": "error",
        "noUnusedImports": "error",
        "useExhaustiveDependencies": "error"
      },
      "style": {
        "noNonNullAssertion": "warn",
        "useConst": "error",
        "useImportType": "error"
      },
      "suspicious": {
        "noExplicitAny": "warn",
        "noConsoleLog": "warn"
      }
    }
  },
  "formatter": {
    "enabled": true,
    "indentStyle": "space",
    "indentWidth": 2,
    "lineWidth": 100
  },
  "javascript": {
    "formatter": {
      "quoteStyle": "double",
      "semicolons": "always",
      "trailingCommas": "all"
    }
  },
  "files": {
    "ignore": ["node_modules", "dist", "build", ".next", "coverage"]
  }
}

Migration workflow

Real-World Use Cases

Monorepo with Turborepo

A fintech company with 47 packages in a Turborepo monorepo replaced ESLint (with 23 custom rules) and Prettier with Biome. CI lint time dropped from 8 minutes to 45 seconds. The single biome.json at the root with package-specific overrides eliminated 47 separate ESLint configurations. Import organization was consistent across all packages without any additional tooling.

React/Next.js Application

A SaaS startup running Next.js 14 with 200+ components migrated from ESLint + Prettier in a single afternoon. Biome's React-specific rules (JSX key warnings, exhaustive hooks dependencies) matched their existing ESLint rule coverage. The developer experience improved noticeably—Biome's language server provides instant feedback in VS Code, compared to the 2-3 second delay they experienced with ESLint's language server.

Open Source Library

An open source state management library adopted Biome for its 15,000-line codebase. The biome ci command in GitHub Actions replaced their multi-step lint/format/type-check pipeline. Contributors no longer needed to configure ESLint or Prettier—running biome check --write before committing ensured consistent code style.

Enterprise TypeScript Monolith

A 500,000-line TypeScript enterprise application migrated from ESLint with TypeScript parser. Biome's native TypeScript support eliminated the need for @typescript-eslint/parser and its associated performance overhead. Full-project linting dropped from 12 minutes to 90 seconds, enabling developers to run lint locally instead of waiting for CI.

Best Practices for Production

  1. Start with recommended rules — Biome's recommended rule set covers 90% of common issues. Enable it as your baseline, then customize individual rules based on your team's preferences.

  2. Use biome check instead of separate lint + format — The check command runs linting, formatting, and import organization in a single pass. It's faster than running them separately and catches format violations that lint-only runs miss.

  3. Leverage the VS Code extension — Biome's VS Code extension provides real-time linting and formatting as you type. Configure it as the default formatter for JavaScript and TypeScript files to get instant feedback.

  4. Use biome ci in GitHub Actions — The ci command is optimized for CI environments. It runs all checks in parallel across files and produces machine-readable output for CI annotations.

  5. Disable rules gradually — When migrating from ESLint, don't try to match every rule immediately. Start with Biome's recommended set, fix violations, then selectively enable additional rules over time.

  6. Use --unsafe flag cautiously — Biome's auto-fix is conservative by default. The --unsafe flag enables more aggressive fixes that might change code semantics. Always review unsafe fixes before committing.

  7. Configure per-directory overrides for mixed codebases — Use nested biome.json files to apply different rules to test files, generated code, or legacy modules. This enables incremental migration.

  8. Keep biome.json at project root — Biome resolves configuration by walking up the directory tree. Placing biome.json at the root ensures consistent behavior regardless of which directory a developer runs Biome from.

Common Pitfalls and Solutions

PitfallImpactSolution
Missing ESLint plugin equivalentsLint rules you relied on have no Biome equivalent yetUse biome lint for available rules, keep a minimal ESLint config for gaps
Formatter disagreementsBiome formats differently than Prettier in some casesAccept Biome's formatting or adjust biome.json settings to match preferences
Slow first run on large projectsFirst run parses all files from scratchBiome caches parsed ASTs—subsequent runs are 10x faster
Import sorting conflictsBiome's import organization differs from @trivago/prettier-plugin-sort-importsConfigure organizeImports rules to match your preferred import order
Breaking CI pipelinesReplacing ESLint scripts with Biome in CIMigrate incrementally—run both tools in parallel during transition period
IDE conflictsESLint extension and Biome extension both activeDisable ESLint extension for files where Biome is active

Performance Optimization

Benchmark Results

Biome's performance advantage is dramatic. On a real-world monorepo with 3,000 TypeScript files:

OperationESLint + PrettierBiome 2.0Speedup
Lint all files45s3.2s14x
Format all files38s1.8s21x
Lint + Format83s4.5s18x
CI check (changed files)12s0.4s30x

Caching Strategy

Biome caches parsed ASTs between runs. The first run in a fresh checkout parses all files, but subsequent runs only re-parse changed files. In CI, restore the Biome cache from your CI cache to skip the cold-start penalty.

Parallel Processing

Biome distributes linting and formatting across all available CPU cores. On an 8-core machine, it processes 8 files simultaneously. This parallelism is built-in—no configuration required.

Comparison with Alternatives

FeatureBiome 2.0ESLint + PrettierDeno Lint + dprintRome (legacy)
LanguageRustJavaScriptRustRust (abandoned)
Lint + FormatSingle toolTwo toolsTwo toolsSingle tool
Multi-file analysisYes (2.0)LimitedNoNo
SpeedVery fastSlowFastFast
Plugin ecosystemGrowingMassiveLimitedN/A
ConfigurationSingle JSONMultiple config filesMultiple config filesSingle JSON
TypeScript supportNativeVia parser pluginNativeNative
Import organizationBuilt-inPlugin requiredNoBuilt-in

Advanced Patterns

Custom Lint Rules (via suppressions)

While Biome doesn't yet support custom lint rules via plugins, you can use inline suppression comments to handle edge cases:

// biome-ignore lint/style/noNonNullAssertion: validated upstream
const user = getUser(id)!;
 
// biome-ignore lint/correctness/noUnusedVariables: used by framework
const _internal = createInternalState();

Monorepo Configuration

// Root biome.json
{
  "$schema": "https://biomejs.dev/schemas/2.0.0/schema.json",
  "linter": {
    "rules": {
      "recommended": true
    }
  }
}
 
// packages/api/biome.json (override for API package)
{
  "linter": {
    "rules": {
      "suspicious": {
        "noConsoleLog": "off"
      }
    }
  }
}
 
// packages/generated/biome.json (disable for generated code)
{
  "linter": {
    "enabled": false
  },
  "formatter": {
    "enabled": false
  }
}

GitHub Actions Integration

# .github/workflows/biome.yml
name: Biome
on: [push, pull_request]
 
jobs:
  biome:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: yarn
      - run: yarn install --frozen-lockfile
      - run: npx biome ci .

Testing Strategies

Biome itself is extensively tested, but you should verify your configuration works correctly:

# Check that biome.json is valid
npx biome check --max-diagnostics=100 .
 
# Verify formatting is consistent
npx biome format --check .
 
# Run only lint rules
npx biome lint .
 
# Check specific files
npx biome check src/components/UserCard.tsx
 
# Generate a configuration report
npx biome rage

Future Outlook

Biome's roadmap is ambitious. The team plans to add CSS linting and formatting, HTML support, and a full-featured bundler—effectively replacing the entire frontend toolchain with a single Rust binary. The 2.0 release's multi-file analysis is the foundation for more advanced rules like detecting circular dependencies, enforcing module boundaries, and analyzing dead code across project boundaries.

The plugin system is another high-priority feature. While Biome's built-in rules cover most use cases, teams with domain-specific lint needs (e.g., enforcing internal API conventions) require custom rules. The Biome team has committed to a WASM-based plugin system that maintains Biome's performance characteristics.

The broader trend toward Rust-based JavaScript tooling (Vite's Rolldown, OXC, Rspack) suggests Biome is riding a wave rather than leading a niche. As these tools mature and interoperate, the fragmented JavaScript toolchain of 2020 may finally consolidate into a coherent, fast, and maintainable system.

Performance Monitoring in Production

Setting up comprehensive performance monitoring ensures that your optimizations continue to deliver value after deployment. Without monitoring, performance regressions can silently accumulate as your application evolves, eventually degrading user experience below acceptable thresholds.

Real User Monitoring (RUM)

Real User Monitoring captures performance metrics from actual users in production environments, providing data that synthetic benchmarks cannot replicate. Implement RUM by collecting Core Web Vitals metrics from the web-vitals library and sending them to your analytics platform:

import { onCLS, onFID, onLCP, onINP, onTTFB } from 'web-vitals';
 
function sendToAnalytics(metric) {
  const body = JSON.stringify({
    name: metric.name,
    value: metric.value,
    rating: metric.rating,
    delta: metric.delta,
    id: metric.id,
    navigationType: metric.navigationType,
    page: window.location.pathname,
    connection: navigator.connection?.effectiveType,
    deviceMemory: navigator.deviceMemory,
  });
 
  // Use Beacon API for reliable delivery even during page unload
  if (navigator.sendBeacon) {
    navigator.sendBeacon('/api/vitals', body);
  } else {
    fetch('/api/vitals', { body, method: 'POST', keepalive: true });
  }
}
 
onCLS(sendToAnalytics);
onFID(sendToAnalytics);
onLCP(sendToAnalytics);
onINP(sendToAnalytics);
onTTFB(sendToAnalytics);

Performance Budgets

Establish performance budgets that prevent regressions from reaching production. Configure your CI pipeline to fail builds that exceed these budgets:

{
  "budgets": [
    {
      "type": "initial",
      "maximumWarning": "200kb",
      "maximumError": "250kb"
    },
    {
      "type": "bundle",
      "name": "vendor",
      "maximumWarning": "150kb",
      "maximumError": "200kb"
    }
  ]
}

Track bundle size changes in pull requests using tools like bundlewatch or size-limit. These tools compare the bundle size of the current branch against the base branch and report differences directly in the PR, making it easy to identify which changes introduced significant size increases.

Continuous Performance Regression Testing

Integrate Lighthouse CI into your deployment pipeline to catch performance regressions before they reach production. Configure it to run against key pages and fail the build if any metric drops below your defined thresholds:

# lighthouserc.js
module.exports = {
  ci: {
    collect: {
      url: ['http://localhost:3000/', 'http://localhost:3000/dashboard'],
      numberOfRuns: 3,
    },
    assert: {
      assertions: {
        'categories:performance': ['error', { minScore: 0.9 }],
        'categories:accessibility': ['error', { minScore: 0.95 }],
        'largest-contentful-paint': ['error', { maxNumericValue: 2500 }],
        'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }],
      },
    },
  },
};

This automated approach ensures that every deployment maintains your performance standards, preventing the gradual degradation that occurs when performance is only manually tested.

Community Resources and Further Learning

The technology landscape evolves rapidly, making continuous learning essential for maintaining expertise. Building a systematic approach to staying current with developments in your technology stack ensures you can leverage new features and avoid deprecated patterns.

Curated Learning Pathways

Rather than consuming content randomly, create structured learning pathways aligned with your current projects and career goals. Start with official documentation and specification documents, which provide the most accurate and comprehensive information. Follow this with hands-on tutorials and workshops that reinforce concepts through practical application.

Technical blogs from framework maintainers and core team members often provide deeper insights into design decisions and upcoming features. Subscribe to the official blogs of your primary frameworks and libraries to stay ahead of breaking changes and deprecation timelines.

Contributing to Open Source

Contributing to open-source projects in your technology stack provides unparalleled learning opportunities. Start with documentation improvements and bug reports, then progress to fixing small issues tagged as "good first issue" in your favorite projects. This direct engagement with maintainers and the codebase accelerates your understanding far beyond what passive learning can achieve.

# Setting up for contribution
git clone https://github.com/project/repository.git
cd repository
git checkout -b fix/issue-description
 
# Run the project's contribution setup
npm run setup:dev
npm run test  # Ensure tests pass before making changes
 
# Make your changes, then run the full test suite
npm run test:full
npm run lint
npm run build
 
# Submit your contribution
git add -A
git commit -m "fix: description of the fix
 
Closes #1234"
git push origin fix/issue-description

Building a Technical Knowledge Base

Maintain a personal knowledge base that captures insights, solutions, and patterns you discover during your work. Tools like Obsidian, Notion, or even a simple Markdown repository can serve as an external memory that grows more valuable over time.

Organize your notes by topic rather than chronologically, and include code examples, links to relevant documentation, and explanations of why certain approaches work better than others. When you encounter a particularly insightful article or conference talk, write a summary that captures the key takeaways and how they apply to your current projects.

Follow key conferences and their published talks to stay informed about emerging patterns and best practices. Many conferences publish recorded talks on YouTube within weeks of the event, making world-class technical content freely accessible.

Join relevant Discord servers, Slack communities, and forums where practitioners discuss real-world challenges and solutions. These communities provide early warning about emerging issues and access to collective wisdom that isn't available through formal documentation.

Mentorship and Knowledge Sharing

Teaching others is one of the most effective ways to deepen your own understanding. Consider writing technical blog posts, giving talks at local meetups, or mentoring junior developers. The process of explaining concepts to others forces you to organize your knowledge and identify gaps in your understanding.

Pair programming sessions with colleagues of different experience levels create mutual learning opportunities. Senior developers gain fresh perspectives on problems they've solved the same way for years, while junior developers benefit from exposure to production-grade thinking and decision-making processes.

Conclusion

Biome 2.0 is not just a faster ESLint—it's a rethinking of what a JavaScript toolchain should be. The key takeaways:

  1. One tool replaces three — Linting, formatting, and import organization in a single pass eliminates configuration drift and redundant parsing
  2. Speed is transformative — 10-30x faster than ESLint + Prettier changes how you interact with your toolchain, making local linting practical
  3. Multi-file analysis unlocks new rules — Project-level understanding enables rules that consider cross-file dependencies and usage patterns
  4. Migration is incremental — Biome's migration tool and compatible rule sets mean you don't have to switch everything at once
  5. The ecosystem is maturing fast — VS Code extension, CI integration, and monorepo support make Biome production-ready today

Start by running npx @biomejs/biome check . on your project. If the output is manageable, migrate. If it's overwhelming, start with formatting only (biome format --write .) and add linting rules incrementally. The speed improvement alone justifies the switch.