FallbackWatcher: `#unregisterDir` rescans the whole directory registry per unlink, stalling the dev server on Windows
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 startruns 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:
NativeWatcheris macOS-only, so Windows and Linux always getFallbackWatcher:static isSupported() { return platform() === "darwin"; }android/app/.cxxis 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
- Windows, no Watchman installed.
npx @react-native-community/cli init Repro(RN 0.87.1), no custommetro.config.js.npx react-native startnpx react-native run-android— this createsandroid/app/.cxxwith the CMake/ninja
build tree, inside the watched root.- 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
- Index
#dirRegistryso subtree removal is not a full scan — a prefix tree, or a
parent→children map, making#unregisterDirproportional to the subtree. - Skip the work when nothing is registered under the path. The hot path is a file
that vanished, where#unregisterDircan only ever return an empty array. A cheap guard
(if (!this.#dirRegistry[fullPath]) return []) would remove most of the cost without
changing behaviour. - Ship a default
blockListcovering 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. - Consider extending
NativeWatcherbeyonddarwin, 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
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- 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.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của react/metro
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
react/metro#1927 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 3/5 1-2 ngày Mức phù hợp với người mới 68/100
Maintainer thường phản hồi trong vòng 1 ngày
-
Async chunk URLs drop the file extension, so .cjs async imports fail with "Requiring unknown module"Đang mở
Độ khó 3/5 1-2 ngày Mức phù hợp với người mới 74/100
react/metro#1959 · 2 bình luận · 2 reaction ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 4/5 3-5 ngày Mức phù hợp với người mới 55/100
Maintainer thường phản hồi trong vòng 1 ngày
-
Please support Yarn PnPĐang mở
Độ khó 5/5 Hơn một tuần Mức phù hợp với người mới 35/100
react/metro#1928 · 2 bình luận · 2 reaction ·
Maintainer thường phản hồi trong vòng 1 ngày
Issue tương tự
-
[Bug] @deck.gl/arcgis dist import resolves to unpublished @deck.gl/core source path (9.3.11, 9.4.0)Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 72/100
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 76/100
stac-utils/stac-fields#42 ·
-
[sec-check] SVG remote-reference gate misses src attributes, link hrefs and bare-string @importĐang mởagent/security hive/hosted-available-lke648397-260827-5n31 security
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 88/100
Maintainer thường phản hồi trong vòng 1 ngày
-
Fix #3381 does not fix #3343: watch() returns the ref, not the value, so setup() never re-runsĐang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 86/100
Maintainer thường phản hồi trong vòng 1 ngày
-
Add: Eurosport 1 PolskaĐang mởchannels:add check:passed
Độ khó 1/5 Dưới một giờ Mức phù hợp với người mới 75/100
Maintainer thường phản hồi trong vòng 2 ngày