FallbackWatcher: `#unregisterDir` rescans the whole directory registry per unlink, stalling the dev server on Windows
维护者通常 2 天内回复
还没有人认领这个 Issue。
- #1925 来自 @kkkhs —— 已关闭,未合并
评估
- 难度
- 3/5
- 预计耗时
- 1-2 天
- 新手友好度
- 68/100
- Issue 类型
- 缺陷
- 描述清晰度
- 基本清楚
- 活跃度
- 活跃
- 技术栈
- javascript, node.js, react-native
- 领域
- performance, tooling
调研方向
从 packages/metro-file-map/src/watchers/FallbackWatcher.js 开始,阅读 #unregisterDir 及其从 #normalizeChange 发起的调用。复现或检查路径消失时的错误流程,然后选择一种修复方案,避免针对文件 unlink 事件扫描整个目录注册表,同时保留子树移除行为。当 watcher 能够处理瞬时删除,且不会反复扫描整个注册表时,即表示完成。
由索引模型根据 Issue 内容生成。
描述
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"
- 主要语言
- JavaScript
- 星标
- 5.7k
- 派生
- 711
- 平均合并
- 2 天 14 小时
- 30 天内合并 PR
- 67
环境准备
- 没有 Dockerfile 或 Docker Compose 文件
- 有 Pull Request 模板
- 阅读贡献指南
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
react/metro 的其他 Issue
-
难度 4/5 3-5 天 新手友好度 55/100
维护者通常 2 天内回复
-
难度 4/5 3-5 天 新手友好度 25/100
维护者通常 2 天内回复
-
难度 4/5 1-2 天 新手友好度 35/100
维护者通常 2 天内回复
-
[0.83.x] Backport micromatch removal and dotfile/multipart watcher fix together可能已有人在做 关联的 PR 仍在进行中或已合并。 未关闭
难度 4/5 3-5 天 新手友好度 45/100
维护者通常 2 天内回复
-
`dependencies is not iterable` / `Cannot read property 'reduce' of undefined`: `FileStore` serves zero-prefixed cache files as hits可能已有人在做 @maximilliangrand 于 3 天前认领。 未关闭
难度 3/5 1-2 天 新手友好度 68/100
维护者通常 2 天内回复
相似的 Issue
-
难度 2/5 1-3 小时 新手友好度 76/100
Graphify-Labs/graphify#4860 · 1 条评论 ·
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 65/100
CommandCodeAI/command-code#1088 ·
维护者通常 2 天内回复
-
难度 2/5 1-3 小时 新手友好度 78/100
CanadaHonk/porffor#426 ·
-
难度 2/5 1-3 小时 新手友好度 66/100
-
bug
难度 2/5 1-3 小时 新手友好度 66/100
RIT-Software-Engineering/RIT-SE-Senior-Project#546 ·
维护者通常 1 天内回复