From 680aaf53783a2c055351f4b80ed681b28a4ca317 Mon Sep 17 00:00:00 2001 From: Rob Hogan <2590098+robhogan@users.noreply.github.com> Date: Wed, 11 Nov 2026 18:37:20 +0000 Subject: [PATCH] metro-file-map: Remove FallbackWatcher's 1s startup delay on Windows On Windows, `FallbackWatcher` waits a second after setting up its watches: https://github.com/react/metro/blob/56efab302200568c919ac1b3c28dacf8d84fc692/packages/metro-file-map/src/watchers/FallbackWatcher.js#L77-L82 That's a holdover from `sane`. libuv issues `ReadDirectoryChangesW` before `uv_fs_event_start` returns, so a watch is live as soon as `fs.watch` returns (here in libuv 1.49.2, which Node 22.13 ships): https://github.com/libuv/libuv/blob/e1095c7a4373ce00cd8874d8e820de5afb25776e/src/win/fs-event.c#L298-L320 ## Benchmark Unsurprisingly, this takes ~exactly one second off watch-mode startup on Windows (1,941ms -> 935ms), and is a no-op elsewhere. | | `main` | #2105 | This PR | vs `main` | vs #2105 | |---|---|---|---|---|---| | Linux: Watch-mode start, warm cache | 1,887ms | 975ms | 969ms | -48.7% (-49.2% to -47.9%) | -0.7% (-1.4% to +1.2%) | | Windows: Watch-mode start, warm cache | 2,577ms | 1,941ms | 935ms | -63.7% (-63.9% to -63.6%) | -51.8% (-52.0% to -51.7%) | | Linux: Watch-mode start, cold cache | 1,885ms | 921ms | 931ms | -50.6% (-51.1% to -49.8%) | +1.1% (-0.2% to +2.6%) | | Windows: Watch-mode start, cold cache | 2,562ms | 1,918ms | 917ms | -64.2% (-64.4% to -64.0%) | -52.2% (-52.5% to -52.1%) | ## Changelog ``` - **[Performance]**: metro-file-map: Remove a 1s delay from file watcher startup on Windows without Watchman ``` ## Test plan New unit test that changes made as soon as `startWatching` resolves are reported. It runs on Linux and Windows, and is skipped on macOS, where `fs.watch` starts FSEvents asynchronously (but `FallbackWatcher` isn't used). Benchmark as in #2104. --- .../src/watchers/FallbackWatcher.js | 6 ---- .../__tests__/FallbackWatcher-test.js | 29 +++++++++++++++++++ 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/packages/metro-file-map/src/watchers/FallbackWatcher.js b/packages/metro-file-map/src/watchers/FallbackWatcher.js index a805afab5d..f0a97106c8 100644 --- a/packages/metro-file-map/src/watchers/FallbackWatcher.js +++ b/packages/metro-file-map/src/watchers/FallbackWatcher.js @@ -79,12 +79,6 @@ export default class FallbackWatcher extends AbstractWatcher { this.#checkedEmitError, this.ignored, ); - - if (platform === 'win32') { - // Inherited from sane: win32 needs a settling period after the watches - // are registered before events can be relied on. - await new Promise(resolve => setTimeout(resolve, 1000)); - } } /** diff --git a/packages/metro-file-map/src/watchers/__tests__/FallbackWatcher-test.js b/packages/metro-file-map/src/watchers/__tests__/FallbackWatcher-test.js index 861d54c7bf..3d734b176b 100644 --- a/packages/metro-file-map/src/watchers/__tests__/FallbackWatcher-test.js +++ b/packages/metro-file-map/src/watchers/__tests__/FallbackWatcher-test.js @@ -168,6 +168,35 @@ describe('FallbackWatcher', () => { await expect(watcher?.startWatching()).resolves.toBeUndefined(); }); + // Every watch is live once `startWatching` resolves, Windows included. + // Skipped on macOS, where `fs.watch` starts FSEvents streams asynchronously + // (FallbackWatcher isn't used there). + (process.platform === 'darwin' ? test.skip : test)( + 'reports changes made as soon as it has started', + async () => { + const dirs = Array.from({length: 20}, (_, i) => join(watchRoot, `d${i}`)); + for (const dir of dirs) { + await mkdir(dir); + } + const touched = new Set(); + watcher?.onFileEvent(event => { + if (event.event === 'touch' && event.metadata?.type === 'f') { + touched.add(event.relativePath); + } + }); + + await watcher?.startWatching(); + for (const dir of dirs) { + await writeFile(join(dir, 'file.js'), ''); + } + + await waitFor(() => touched.size === dirs.length); + expect(touched).toEqual( + new Set(dirs.map((_, i) => join(`d${i}`, 'file.js'))), + ); + }, + ); + // A watch we cannot establish - one directory over the inotify limit, say - // must not cost us the files under it, which would silently truncate the // file map.