Hacktoberfest 2026: những issue maintainer đã đánh dấu cho tháng Mười, đang mở và phù hợp người mới. Xem issue Hacktoberfest

FallbackWatcher: `#unregisterDir` rescans the whole directory registry per unlink, stalling the dev server on Windows

Đang mở
#1,915 1 bình luận 0 reaction 0 người được giao Xem trên GitHub

Maintainer thường phản hồi trong vòng 1 ngày

Chưa có ai nhận issue này.

Đánh giá

Độ khó
3/5
Thời gian dự kiến
1-2 ngày
Mức phù hợp với người mới
68/100
Loại issue
Lỗi
Độ rõ ràng
Khá rõ ràng
Mức độ hoạt động
Sôi nổi
Công nghệ
javascript, node.js, react-native
Lĩnh vực
performance, tooling

Hướng nghiên cứu

Bắt đầu tại packages/metro-file-map/src/watchers/FallbackWatcher.js, đọc #unregisterDir và lời gọi của nó từ #normalizeChange. Tái hiện hoặc kiểm tra luồng lỗi của đường dẫn đã biến mất, sau đó chọn một cách sửa để tránh quét toàn bộ registry thư mục cho các sự kiện unlink tệp, đồng thời vẫn giữ nguyên hành vi xóa cây con. Hoàn thành khi watcher xử lý được các lần xóa tạm thời mà không lặp lại việc quét toàn bộ registry.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Mô tả

Summary

On any platform without Watchman, metro-file-map falls back to FallbackWatcher. Its
#unregisterDir() iterates every key in the directory registry on every unlink-ish
event. In a React Native project whose native build tree sits inside the watched root — the
default for Android, android/app/.cxx — CMake/ninja churn produces tens of thousands of
transient file deletions, so this becomes an O(registry × events) scan that saturates the
event loop.

The user-visible result is not an error. Metro keeps serving, just slowly enough that
anything latency-sensitive breaks. In our case React Native DevTools opened to a permanently
blank window, because the CDP traffic it needs at startup was arriving ~2000 ms per request
instead of single-digit milliseconds.

Environment

metro, metro-file-map, metro-config 0.87.0
react-native 0.87.1
@react-native/dev-middleware 0.87.1
Node 22.23.2
OS Windows 11
Watchman not installed
Architecture New Architecture enabled (Fabric + TurboModules)

Symptom

  • npx react-native start runs and bundles successfully (24.5 MB bundle, no errors).
  • React Native DevTools (Electron shell, and the same frontend opened manually in Chrome)
    shows a blank window — white, grey, then white — and never populates.
  • The Metro process sits at high CPU while the project is completely idle.
  • Simple HTTP requests to the dev server take ~2000 ms.

The blank DevTools window is the misleading part: it looks like a DevTools bug, and there are
open reports that describe the same surface symptom without identifying a cause (see
Possibly related below). It is a latency problem in the dev server.

Root cause

packages/metro-file-map/src/watchers/FallbackWatcher.js:

#unregisterDir(dirpath) {
  const removedFiles = [];
  for (const registeredDir of Object.keys(this.#dirRegistry)) {   // <-- full scan
    if (
      registeredDir === dirpath ||
      registeredDir.startsWith(dirpath + path.sep)
    ) {
      for (const filename of Object.keys(this.#dirRegistry[registeredDir])) {
        removedFiles.push(path.join(registeredDir, filename));
      }
      delete this.#dirRegistry[registeredDir];
    }
  }
  return removedFiles;
}

#dirRegistry is a flat map keyed by absolute directory path, holding every watched
directory in the root (including node_modules). Finding the subtree under dirpath
requires a linear scan with a startsWith per key, so the cost is proportional to the size
of the whole registry — not to the size of the subtree being removed.

It is called from the error path of #normalizeChange:

} catch (error) {
  if (!isIgnorableFileError(error)) { ... }
  this.#unregister(fullPath);
  const removedFiles = this.#unregisterDir(fullPath);   // <-- every vanished path
  ...
}

Any watched path that has already gone by the time it is stated lands here — which is the
normal case for a build tool writing and deleting temporary files. So the full-registry scan
runs once per transient file, not once per directory removal.

Two things make this reliably bad for React Native on Windows:

  1. NativeWatcher is macOS-only, so Windows and Linux always get FallbackWatcher:
    static isSupported() {
      return platform() === "darwin";
    }
    
  2. android/app/.cxx is inside the watched root and is not excluded by any default
    blockList. It is the single largest churn source in a default RN Android project.

Evidence

Node --cpu-prof of the Metro process while the project was idle:

  • 88.7% of samples in #unregisterDir (FallbackWatcher).

Instrumenting the watcher's event emission over one idle two-minute window:

  • 62,389 events, all originating from android/app/.cxx/Debug/<hash>.

After excluding .cxx from the blockList (workaround below), with nothing else changed:

  • Dev server response time: ~2000 ms → 7–15 ms.
  • DevTools opens and populates normally.
  • Metro CPU at idle drops to nil.

Reproduction

  1. Windows, no Watchman installed.
  2. npx @react-native-community/cli init Repro (RN 0.87.1), no custom metro.config.js.
  3. npx react-native start
  4. npx react-native run-android — this creates android/app/.cxx with the CMake/ninja
    build tree, inside the watched root.
  5. Leave the project idle. Metro's CPU stays high; curl -w "%{time_total}" against the dev
    server shows seconds-scale responses; DevTools opens blank.

Larger projects hit this harder, since the cost scales with the registry size.

Workaround

Exclude native build output from the watched set. With @rnx-kit/metro-config, note that
makeMetroConfig replaces its own blockList when you supply one, so exclusionList has to
re-add the defaults:

const { makeMetroConfig, exclusionList } = require('@rnx-kit/metro-config');

const buildOutputDirs = [
    // `.cxx` anywhere: the CMake/ninja native build tree. Measured as the single
    // biggest churn source -- 62,389 watcher events from `android/app/.cxx/Debug/<hash>`
    // in one idle two-minute window.
    /[/\\]\.cxx[/\\].*/,
    /[/\\]android[/\\]\.gradle[/\\].*/,
    /[/\\]android[/\\]build[/\\].*/,
    /[/\\]android[/\\]app[/\\]build[/\\].*/,
    /[/\\]ios[/\\]build[/\\].*/,
    /[/\\]ios[/\\]DerivedData[/\\].*/,
];

module.exports = makeMetroConfig({
    resolver: { blockList: exclusionList(buildOutputDirs) },
});

Installing Watchman also avoids it, by not using FallbackWatcher at all.

Worth noting for anyone else debugging this: the pattern must match the real path. Ours was
initially /android/.cxx/ while the directory is android/app/.cxx, which cut the latency
only ~3x and made it look like the diagnosis was wrong.

Suggested fixes

  1. Index #dirRegistry so subtree removal is not a full scan — a prefix tree, or a
    parent→children map, making #unregisterDir proportional to the subtree.
  2. Skip the work when nothing is registered under the path. The hot path is a file
    that vanished, where #unregisterDir can only ever return an empty array. A cheap guard
    (if (!this.#dirRegistry[fullPath]) return []) would remove most of the cost without
    changing behaviour.
  3. Ship a default blockList covering native build output (.cxx, android/build,
    android/app/build, android/.gradle, ios/build, ios/DerivedData). Metro watching
    its own project's build tree is never useful, and this would fix the common case for
    everyone rather than only those who find the workaround.
  4. Consider extending NativeWatcher beyond darwin, or documenting clearly that
    Windows and Linux users without Watchman are on a watcher with this cost profile.

(1) or (2) is the real fix; (3) would help every RN project on Windows immediately.

Possibly related

This describes the same blank-DevTools surface symptom without a root cause, and may be the
same bug for the ones on machines without Watchman — worth cross-referencing:

  • react/react-native#57436 — "Reporting a bug for React Native DevTools"
Ngôn ngữ chính
JavaScript
Star
5.6k
Fork
696
Merge trung bình
2 ngày 7 giờ
Pull request đã merge (30 ngày)
28

Chuẩn bị môi trường

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Issue khác của react/metro

Tất cả issue của react/metro

Issue tương tự

Thêm issue về JavaScript

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.