| | |

Biome 27 🧹 Performance — Scanner, watcher, and cache behavior

⏱️ Estimated read: 13 min | |

The Biome performance model has three moving parts: the scanner, which walks the project to find the configuration files and build the module graph; the watcher, which monitors the files that the scanner and the language server care about; and the cache, which reuses the parse results across the runs. The three interact, and the interaction is the source of the performance regressions that the users report. The Biome 2.0 introduced the scanner and the module graph for the type-aware features, and the introduction is the reason the v2 is the slower than the v1 in the some cases . The v2.1 and the v2.2 addressed the specific issues—the scanner scope, the watcher recursion, and the ignore handling—and the specific issues are the reason the performance is the ongoing work.

The most important lesson from the issue tracker is the gap between the CLI and the LSP. The CLI applies the files.includes and the vcs.useIgnoreFile correctly, and the LSP does not—the issue #7388 reports the LSP watching the 730 to the 884 directories, including the 670+ the node_modules directories, despite the explicit exclusion . The CLI checks the 6 files in the 3 milliseconds, and the LSP watches the 147 times more directories. The gap is the reason the performance is the configuration-specific, and the configuration-specific is the reason the debugging is the evidence.

Key point: The scanner’s “light” mode scans only the files and the folders that the CLI arguments point to, and the project rules mode scans the whole project. The watcher uses the non-recursive watchers for the relevant folders, and the recursive watcher was the memory leak. The cache reuses the parse results, and the CI cache is the separate mechanism. The LSP’s watcher may ignore the configuration exclusions in the pnpm monorepos, and the CLI is the fallback.


Why the performance depends on the configuration

The scanner’s two modes problem. The Biome 2.0 introduced the scanner for the nested config discovery and the module graph. The scanner has the two modes: the light mode when the project rules are disabled, and the full mode when the project rules are enabled . The light mode in the v2.0 scanned the whole project from the root, and the whole scan was the overhead even without the project rules. The v2.1 changed the light mode to use the CLI arguments as the hint—the specific files or the nested folder—and the hint is the mechanism that the light scanner scans only the relevant parts . The full mode still scans the whole project because the project rules need the cross-file information, and the full scan is the reason the project rules are the opt-in.

The watcher’s recursion problem. The v2.0 used the single recursive watcher, and the recursive watcher received the notifications for the every file in the project, including the ignored folders like the build/ and the dist/ . The issue #6838 reports the 62,199 the inotify watches, which exhausted the OS limit and slowed the IDE. The v2.1.2 fixed the issue by using the non-recursive watchers for the relevant folders, and the non-recursive is the mechanism that the ignored folders are not the watched .

The ignore-handling problem. The issue #7388 reports the LSP ignoring the !**/node_modules exclusion in the pnpm monorepo, and the LSP watching the 670+ the node_modules directories. The CLI works correctly, and the LSP does not. The v2.1.2 changelog states the fix: the root ignore file is now correctly loaded during the scanning phase, and the vcs.useIgnoreFile globs have the same semantics as the files.includes of the root configuration . The fix addresses the scanner, and the LSP’s watcher behavior is the separate issue .

The cache’s reuse problem. The Biome cache stores the parse results, and the reuse is the mechanism that the unchanged files are not the re-parsed. The v2’s cache is the advanced, and the file content change is the mechanism that the cache is the invalidated . The CI cache is the separate concern—the actions/cache with the package-lock.json key is the mechanism that the npm ci is the fast, and the Biome’s own cache is the separate.

The useImportExtensions problem. The discussion #5633 reports the v2 slower by the 10 times, and the maintainer notes that some rules became the part of the project domain in the v2—the useImportExtensions is the example—and the rules may explain the slowdown . The project domain is the opt-in, and the domain is the reason the type-aware rules are the separate cost.


a. The scanner, the watcher, and the configuration

The scanner’s light mode uses the CLI arguments as the hint. The biome check src/app.ts scans the src and the app.ts, and the biome check from the project root scans the whole project . The biome check packages/logger from the nested folder scans only the packages/logger, and the nested scan is the mechanism that the light mode is the fast.

# The light scanner with the specific file
biome check src/app.ts

# The light scanner with the nested folder
biome check packages/logger

# The full scanner (the project rules enabled)
biome check # the whole project

The biome check src/app.ts is the mechanism that the scanner knows the src is the relevant, and the relevant is the reason the scan is the fast. The biome check packages/logger is the same mechanism with the nested folder. The biome check without the arguments is the whole project, and the whole project is the reason the scan is the slow.

The project domain is the separate. The linter.domains.project: "recommended" is the mechanism that the full scan is the enabled, and the full scan is the reason the type-aware rules are the cost .

// biome.json — the project domain
{
  "linter": {
    "domains": { "project": "recommended" }
  }
}

The project: "recommended" is the mechanism that the whole project is the scanned. The scanner needs the cross-file information for the type-aware rules, and the cross-file is the reason the full scan cannot be the avoided .

The watcher’s non-recursive setup is the v2.1.2 fix. The non-recursive watcher is the mechanism that only the relevant folders are the watched, and the relevant is the reason the ignored folders are not the watched .

# The watcher behavior
The v2.0: the single recursive watcher → the every file → the 62,199 watches
The v2.1.2: the non-recursive watchers → the relevant folders → the reduced watches

The v2.1.2 changelog states the fix: the non-recursive watchers are the setup only for the folders that are the relevant to the project, and the relevant is the mechanism that the memory leak is the resolved .


b. The LSP-versus-CLI gap and the CI cache

The LSP-versus-CLI gap is the configuration-specific issue. The CLI applies the files.includes and the vcs.useIgnoreFile correctly, and the LSP does not—the issue #7388 reports the pnpm monorepo’s node_modules being the watched despite the exclusion . The CLI is the fallback, and the fallback is the mechanism that the performance is the verified.

# The CLI fallback
biome check .

# The LSP debug
# The biome-logs/server.log shows the watch_folder count.
grep -c "watch_folder" ~/Library/Caches/dev.biomejs.biome/biome-logs/server.log.*

The grep -c "watch_folder" is the mechanism that the watched directories are the counted. The count is the reason the LSP’s configuration handling is the verified . The biome-logs is the separate directory, and the directory is the mechanism that the LSP activity is the recorded.

The CI cache is the separate mechanism. The actions/cache with the package-lock.json key is the mechanism that the npm ci is the fast, and the Biome’s own cache is the separate. The biome ci is the read-only pipeline command, and the command is the mechanism that the CI is the fast.

# .github/workflows/ci.yml — the cache
- uses: actions/cache@v4
  with:
    path: ~/.npm
    key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}

- run: npm ci
- run: npx biome ci .

The actions/cache is the mechanism that the dependencies are the cached, and the biome ci is the mechanism that the check is the read-only. The two are the separate, and the separate is the reason the CI is the fast.

The biome ci is the separate from the biome check. The ci command is the read-only, and the check command is the fixable. The ci is the pipeline command, and the command is the mechanism that the CI does not modify the tree.


Complete Example Session

# ============================================
# PART 1: THE LIGHT SCANNER WITH THE SPECIFIC FILE
# ============================================

biome check src/app.ts

# The scanner knows the src is the relevant.
# The scan is the fast.

# ============================================
# PART 2: THE LIGHT SCANNER WITH THE NESTED FOLDER
# ============================================

biome check packages/logger

# The scanner scans only the packages/logger.

# ============================================
# PART 3: THE FULL SCANNER WITH THE PROJECT RULES
# ============================================

# biome.json
{
  "linter": { "domains": { "project": "recommended" } }
}

biome check

# The whole project is the scanned.
# The cross-file information is the reason.

# ============================================
# PART 4: THE WATCHER FIX
# ============================================

# The v2.0: the recursive watcher → the 62,199 watches.
# The v2.1.2: the non-recursive watchers → the relevant folders.

# ============================================
# PART 5: THE LSP WATCH COUNT
# ============================================

grep -c "watch_folder" ~/Library/Caches/dev.biomejs.biome/biome-logs/server.log.*

# The count is the mechanism that the LSP is the verified.

# ============================================
# PART 6: THE CLI FALLBACK
# ============================================

biome check .

# The CLI applies the configuration exclusions correctly.

# ============================================
# PART 7: THE CI CACHE
# ============================================

# .github/workflows/ci.yml
- uses: actions/cache@v4
  with:
    path: ~/.npm
    key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}

# ============================================
# PART 8: THE CI COMMAND
# ============================================

npx biome ci .

# The read-only check in the pipeline.

# ============================================
# PART 9: THE PROJECT DOMAIN COST
# ============================================

# The project domain enables the full scan.
# The useImportExtensions is the example of the project-domain rule.

# ============================================
# PART 10: THE CHECKLIST
# ============================================

# [ ] The light scanner uses the CLI arguments as the hint.
# [ ] The project domain enables the full scan.
# [ ] The non-recursive watcher is the v2.1.2 fix.
# [ ] The LSP may ignore the exclusions in the pnpm monorepos.
# [ ] The CLI is the fallback.
# [ ] The CI cache is the separate mechanism.

The ten parts covered the light scanner with the specific file, the light scanner with the nested folder, the full scanner with the project rules, the watcher fix, the LSP watch count, the CLI fallback, the CI cache, the CI command, the project domain cost, and the checklist. The through-line is that the scanner’s scope is the configuration, and the watcher’s recursion is the separate.


Quick Reference

The scanner modes

ModeTriggerScope
LightThe project rules disabledThe CLI arguments’ files and folders
FullThe project domain enabledThe whole project

The watcher behavior

VersionWatcherIssue
v2.0The recursiveThe 62,199 watches, the OS limit
v2.1.2The non-recursiveThe relevant folders only

The LSP-versus-CLI

AspectCLILSP
The files.includesAppliedThe pnpm bug (the issue #7388)
The vcs.useIgnoreFileAppliedThe fix in the v2.1.2
The node_modulesExcludedThe 670+ watched in the pnpm

The CI cache

MechanismPurpose
The actions/cacheThe dependencies cached
The biome ciThe read-only check
The biome checkThe fixable check

Best Practices

✅ Do This:

# Use the specific file for the light scanner
biome check src/app.ts                                             // ✅

# Use the nested folder for the light scanner
biome check packages/logger                                        // ✅

# Use the CLI as the fallback for the LSP performance
biome check .                                                      // ✅

❌ Don’t Do This:

# Enable the project domain without the need
{ "linter": { "domains": { "project": "recommended" } } } // the full scan // ❌

# Assume the LSP respects the exclusions
// The pnpm monorepo bug watches the node_modules.                 // ❌

Common Pitfalls

PitfallWhy It HappensFix
The check is the slowThe project domain is enabledDisable the domain or scope the files
The LSP is the slowThe pnpm monorepo bugUse the CLI or the update
The watcher exhausts the OSThe recursive watcher (v2.0)Update to the v2.1.2
The CI is the slowThe cache is the missingAdd the actions/cache

Real-World Examples

1. The light scanner with the file

biome check src/app.ts

2. The light scanner with the folder

biome check packages/logger

3. The full scanner

biome check

4. The project domain

{ "linter": { "domains": { "project": "recommended" } } }

5. The LSP watch count

grep -c "watch_folder" ~/Library/Caches/dev.biomejs.biome/biome-logs/server.log.*

6. The CLI fallback

biome check .

7. The CI cache

- uses: actions/cache@v4

8. The CI command

npx biome ci .

9. The watcher fix

The v2.1.2 uses the non-recursive watchers.

10. The useImportExtensions

The project-domain rule is the separate cost.

Visual

The Scanner Modes

┌──────────────────────────────────────────────────────────────┐
│  THE SCANNER MODES                                           │
│                                                              │
│  The light mode:                                             │
│  ┌─────────────────────┐                                    │
│  │ The CLI arguments   │  The specific files or folders     │
│  │ as the hint         │                                    │
│  └─────────────────────┘                                    │
│                                                              │
│  The full mode:                                              │
│  ┌─────────────────────┐                                    │
│  │ The whole project   │  The project rules enabled         │
│  │ scanned             │                                    │
│  └─────────────────────┘                                    │
└──────────────────────────────────────────────────────────────┘

The Watcher Fix

┌──────────────────────────────────────────────────────────────┐
│  THE WATCHER FIX                                             │
│                                                              │
│  The v2.0:                                                   │
│  ┌─────────────────────┐                                    │
│  │ The recursive       │  The 62,199 watches                │
│  │ watcher             │  The OS limit exhausted            │
│  └──────────┬──────────┘                                    │
│             │                                                │
│             ▼                                                    │
│  The v2.1.2:                                                 │
│  ┌─────────────────────┐                                    │
│  │ The non-recursive   │  The relevant folders only         │
│  │ watchers            │  The memory leak fixed             │
│  └─────────────────────┘                                    │
└──────────────────────────────────────────────────────────────┘

The LSP Gap

┌──────────────────────────────────────────────────────────────┐
│  THE LSP GAP                                                 │
│                                                              │
│  The CLI:                                                    │
│  ┌─────────────────────┐                                    │
│  │ The 6 files         │  The 3ms                           │
│  │ checked             │  The exclusions applied            │
│  └─────────────────────┘                                    │
│                                                              │
│  The LSP (the pnpm):                                         │
│  ┌─────────────────────┐                                    │
│  │ The 730-884 dirs    │  The 670+ node_modules             │
│  │ watched             │  The exclusions ignored            │
│  └─────────────────────┘                                    │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
The light scannerThe CLI arguments as the hint
The full scannerThe whole project, the project rules
The watcher fixThe non-recursive (v2.1.2)
The recursive watcher issueThe 62,199 watches
The LSP gapThe pnpm node_modules watched
The CLI fallbackThe exclusions applied
The CI cacheThe actions/cache

Key takeaways:

  • The scanner has the two modes. The light mode uses the CLI arguments as the hint, and the light mode scans only the relevant parts. The full mode scans the whole project because the project rules need the cross-file information . The project domain is the opt-in that enables the full mode.
  • The recursive watcher was the v2.0 problem. The single recursive watcher received the notifications for the every file, including the ignored folders, and the issue #6838 reports the 62,199 watches that exhausted the OS limit. The v2.1.2 fixed the issue by using the non-recursive watchers for the relevant folders .
  • The LSP ignores the exclusions in the pnpm monorepos. The issue #7388 reports the LSP watching the 670+ the node_modules directories despite the explicit exclusion, while the CLI applies the configuration correctly. The CLI is the fallback, and the fallback is the mechanism that the performance is the verified .
  • The CI cache is the separate mechanism. The actions/cache with the package-lock.json key is the mechanism that the npm ci is the fast, and the biome ci is the read-only pipeline command. The two are the separate, and the separate is the reason the CI is the fast .
  • The project domain is the performance cost. The useImportExtensions is the example of the rule that became the part of the project domain in the v2, and the domain is the reason the full scan is the enabled. The domain is the opt-in, and the opt-in is the trade-off against the speed .

Remember: The Biome performance is the scanner, the watcher, and the cache. The scanner’s light mode uses the CLI arguments as the hint, and the full mode scans the whole project. The watcher uses the non-recursive for the relevant folders, and the recursive was the memory leak. The LSP may ignore the exclusions in the pnpm monorepos, and the CLI is the fallback. The CI cache is the separate mechanism.

Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!