Skip to content

metro-file-map: Skip the per-file worker pass when no file needs a worker - #2220

Draft
robhogan wants to merge 1 commit into
pr2219from
pr2220
Draft

robhogan wants to merge 1 commit into
pr2219from
pr2220

Conversation

@robhogan

@robhogan robhogan commented Nov 12, 2026 •

Copy link
Copy Markdown
Collaborator

With Metro's defaults - lazy SHA-1s, no hasteImplModulePath and enableGlobalPackages: false - no file needs a worker at startup. Even so, every crawled file goes through FileProcessor to find that out: a check for node_modules in its path, a call to the Haste plugin's filter, which always says no, and an array for the plugins to run.

This makes HastePlugin.getWorker() return null when the plugin has nothing to compute, so it gets no worker and no per-file data slot (as MockPlugin already does). processBatch then returns straight away when there's no SHA-1 to compute and no plugin runs on batches.

Any plugin after Haste moves down a slot in each file's metadata, so this bumps CACHE_BREAKER.

Benchmark

This takes 13-14% off applying the crawl on a cold start on macOS and Linux (Linux: 84ms -> 73ms), or 4-5% off a cold start on Linux and Windows (Linux: 315ms -> 298ms). macOS's cold starts are too noisy to show it.

On Linux and Windows, a cache write takes one of two times (on Linux, about 14ms or 20ms), and this PR's writes take the shorter one more often (Linux: 12 of 20 runs against 5, Windows: 20 against 8). That's presumably because there's less garbage to collect, but it makes the cache write medians below jumpy. On macOS, writes are 0.1ms slower.

main #2219 This PR vs main vs #2219
macOS: Applying the crawl in a cold start 18.7ms 17.6ms 15.1ms -18.9% (-20.7% to -17.3%) -14.2% (-16.2% to -12.5%)
Linux: Applying the crawl in a cold start 85ms 84ms 73ms -14.1% (-16.4% to -10.2%) -13.2% (-15.5% to -9.7%)
Windows: Applying the crawl in a cold start 47ms 42ms 40ms -15.5% (-18.1% to -11.8%) -6.2% (-13.1% to +0.1%)
macOS: Cache write in a cold start 7.8ms 4.4ms 4.5ms -41.9% (-42.7% to -40.3%) +3.5% (+2.6% to +4.6%)
Linux: Cache write in a cold start 33ms 20ms 14.0ms -57.8% (-59.0% to -42.0%) -30.5% (-32.3% to -5.1%)
Windows: Cache write in a cold start 20ms 13.8ms 9.4ms -54.2% (-55.9% to -52.6%) -32.1% (-34.8% to +2.1%)
macOS: Cold start 129ms 95ms 91ms -29.8% (-38.6% to -24.2%) -4.7% (-16.7% to +4.8%)
Linux: Cold start 646ms 315ms 298ms -53.9% (-54.6% to -53.5%) -5.4% (-7.2% to -4.6%)
Windows: Cold start 362ms 172ms 164ms -54.7% (-55.7% to -53.7%) -4.5% (-5.7% to -2.2%)

Changelog

 - **[Performance]**: metro-file-map: Skip the per-file worker pass when no file needs a worker

Test plan

New unit tests. Benchmark as in #2104, with 22 rounds on macOS.

@robhogan
robhogan added this pull request to stack #2109 November 12, 2026 09:15
robhogan added a commit that referenced this pull request Nov 12, 2026
Without Watchman, a watch-mode start has to be watching before it crawls, so that a change made during the crawl isn't missed. `NativeWatcher` (macOS) isn't live as soon as it starts, so `Watcher` also confirms each root's watch with a health check before the crawl begins:

https://github.com/react/metro/blob/ef50b94fba3bac242d06b20f03c7149987b3c2d6/packages/metro-file-map/src/Watcher.js#L375-L382

That takes about 13ms on macOS, and `build()` only starts it once the cache has been read, although the watch doesn't need anything from the cache.

This starts the watch, and the Watchman capability check (which spawns `watchman`), while the cache is read. The crawl still waits for the confirmed watch, so nothing can be missed that wasn't before. To allow this, `Watcher` takes the cached state in `crawl()` rather than at construction.

## Benchmark

This takes 13% off a watch-mode start on macOS (102ms -> 88ms). Linux and Windows watch with `FallbackWatcher`, which is live as soon as it starts, so they're unchanged, as are starts outside watch mode. With a cold cache there's no read to overlap with.

|  | `main` | #2220 | This PR | vs `main` | vs #2220 |
|---|---|---|---|---|---|
| macOS: Watch-mode start | 130ms | 102ms | 88ms | -32.1% (-32.8% to -31.2%) | -13.0% (-14.0% to -11.3%) |
| Linux: Watch-mode start | 1,889ms | 493ms | 491ms | -74.0% (-74.2% to -73.6%) | -0.5% (-1.4% to +0.5%) |
| Windows: Watch-mode start | 2,569ms | 524ms | 524ms | -79.6% (-79.7% to -79.5%) | +0.1% (-0.5% to +0.6%) |
| macOS: Warm start | 118ms | 88ms | 88ms | -25.1% (-26.4% to -23.6%) | +0.1% (-1.8% to +1.9%) |
| Linux: Warm start | 636ms | 353ms | 353ms | -44.5% (-45.3% to -44.1%) | -0.1% (-1.8% to +1.1%) |
| Windows: Warm start | 355ms | 201ms | 201ms | -43.4% (-43.7% to -42.7%) | +0.0% (-0.9% to +0.7%) |

## Changelog

```
 - **[Performance]**: metro-file-map: Start watching while the cache is read
```

## Test plan

New unit tests. Benchmark as in #2104, with 22 rounds on macOS.
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Nov 12, 2026
robhogan added a commit that referenced this pull request Nov 12, 2026
Without Watchman, a watch-mode start has to be watching before it crawls, so that a change made during the crawl isn't missed. `NativeWatcher` (macOS) isn't live as soon as it starts, so `Watcher` also confirms each root's watch with a health check before the crawl begins:

https://github.com/react/metro/blob/ef50b94fba3bac242d06b20f03c7149987b3c2d6/packages/metro-file-map/src/Watcher.js#L375-L382

That takes about 13ms on macOS, and `build()` only starts it once the cache has been read, although the watch doesn't need anything from the cache.

This starts the watch, and the Watchman capability check (which spawns `watchman`), while the cache is read. The crawl still waits for the confirmed watch, so nothing can be missed that wasn't before. To allow this, `Watcher` takes the cached state in `crawl()` rather than at construction.

## Benchmark

This takes 13% off a watch-mode start on macOS (102ms -> 88ms). Linux and Windows watch with `FallbackWatcher`, which is live as soon as it starts, so they're unchanged, as are starts outside watch mode. With a cold cache there's no read to overlap with.

|  | `main` | #2220 | This PR | vs `main` | vs #2220 |
|---|---|---|---|---|---|
| macOS: Watch-mode start | 130ms | 102ms | 88ms | -32.1% (-32.8% to -31.2%) | -13.0% (-14.0% to -11.3%) |
| Linux: Watch-mode start | 1,889ms | 493ms | 491ms | -74.0% (-74.2% to -73.6%) | -0.5% (-1.4% to +0.5%) |
| Windows: Watch-mode start | 2,569ms | 524ms | 524ms | -79.6% (-79.7% to -79.5%) | +0.1% (-0.5% to +0.6%) |
| macOS: Warm start | 118ms | 88ms | 88ms | -25.1% (-26.4% to -23.6%) | +0.1% (-1.8% to +1.9%) |
| Linux: Warm start | 636ms | 353ms | 353ms | -44.5% (-45.3% to -44.1%) | -0.1% (-1.8% to +1.1%) |
| Windows: Warm start | 355ms | 201ms | 201ms | -43.4% (-43.7% to -42.7%) | +0.0% (-0.9% to +0.7%) |

## Changelog

```
 - **[Performance]**: metro-file-map: Start watching while the cache is read
```

## Test plan

New unit tests. Benchmark as in #2104, with 22 rounds on macOS.
…rker

With Metro's defaults - lazy SHA-1s, no `hasteImplModulePath` and `enableGlobalPackages: false` - no file needs a worker at startup. Even so, every crawled file goes through `FileProcessor` to find that out: a check for `node_modules` in its path, a call to the Haste plugin's filter, which always says no, and an array for the plugins to run.

This makes `HastePlugin.getWorker()` return `null` when the plugin has nothing to compute, so it gets no worker and no per-file data slot (as `MockPlugin` already does). `processBatch` then returns straight away when there's no SHA-1 to compute and no plugin runs on batches.

Any plugin after Haste moves down a slot in each file's metadata, so this bumps `CACHE_BREAKER`.

## Benchmark

This takes 13-14% off applying the crawl on a cold start on macOS and Linux (Linux: 84ms -> 73ms), or 4-5% off a cold start on Linux and Windows (Linux: 315ms -> 298ms). macOS's cold starts are too noisy to show it.

On Linux and Windows, a cache write takes one of two times (on Linux, about 14ms or 20ms), and this PR's writes take the shorter one more often (Linux: 12 of 20 runs against 5, Windows: 20 against 8). That's presumably because there's less garbage to collect, but it makes the cache write medians below jumpy. On macOS, writes are 0.1ms slower.

|  | `main` | #2219 | This PR | vs `main` | vs #2219 |
|---|---|---|---|---|---|
| macOS: Applying the crawl in a cold start | 18.7ms | 17.6ms | 15.1ms | -18.9% (-20.7% to -17.3%) | -14.2% (-16.2% to -12.5%) |
| Linux: Applying the crawl in a cold start | 85ms | 84ms | 73ms | -14.1% (-16.4% to -10.2%) | -13.2% (-15.5% to -9.7%) |
| Windows: Applying the crawl in a cold start | 47ms | 42ms | 40ms | -15.5% (-18.1% to -11.8%) | -6.2% (-13.1% to +0.1%) |
| macOS: Cache write in a cold start | 7.8ms | 4.4ms | 4.5ms | -41.9% (-42.7% to -40.3%) | +3.5% (+2.6% to +4.6%) |
| Linux: Cache write in a cold start | 33ms | 20ms | 14.0ms | -57.8% (-59.0% to -42.0%) | -30.5% (-32.3% to -5.1%) |
| Windows: Cache write in a cold start | 20ms | 13.8ms | 9.4ms | -54.2% (-55.9% to -52.6%) | -32.1% (-34.8% to +2.1%) |
| macOS: Cold start | 129ms | 95ms | 91ms | -29.8% (-38.6% to -24.2%) | -4.7% (-16.7% to +4.8%) |
| Linux: Cold start | 646ms | 315ms | 298ms | -53.9% (-54.6% to -53.5%) | -5.4% (-7.2% to -4.6%) |
| Windows: Cold start | 362ms | 172ms | 164ms | -54.7% (-55.7% to -53.7%) | -4.5% (-5.7% to -2.2%) |

## Changelog

```
 - **[Performance]**: metro-file-map: Skip the per-file worker pass when no file needs a worker
```

## Test plan

New unit tests. Benchmark as in #2104, with 22 rounds on macOS.
robhogan added a commit that referenced this pull request Nov 12, 2026
Without Watchman, a watch-mode start has to be watching before it crawls, so that a change made during the crawl isn't missed. `NativeWatcher` (macOS) isn't live as soon as it starts, so `Watcher` also confirms each root's watch with a health check before the crawl begins:

https://github.com/react/metro/blob/ef50b94fba3bac242d06b20f03c7149987b3c2d6/packages/metro-file-map/src/Watcher.js#L375-L382

That takes about 13ms on macOS, and `build()` only starts it once the cache has been read, although the watch doesn't need anything from the cache.

This starts the watch, and the Watchman capability check (which spawns `watchman`), while the cache is read. The crawl still waits for the confirmed watch, so nothing can be missed that wasn't before. To allow this, `Watcher` takes the cached state in `crawl()` rather than at construction.

## Benchmark

This takes 13% off a watch-mode start on macOS (102ms -> 88ms). Linux and Windows watch with `FallbackWatcher`, which is live as soon as it starts, so they're unchanged, as are starts outside watch mode. With a cold cache there's no read to overlap with.

|  | `main` | #2220 | This PR | vs `main` | vs #2220 |
|---|---|---|---|---|---|
| macOS: Watch-mode start | 130ms | 102ms | 88ms | -32.1% (-32.8% to -31.2%) | -13.0% (-14.0% to -11.3%) |
| Linux: Watch-mode start | 1,889ms | 493ms | 491ms | -74.0% (-74.2% to -73.6%) | -0.5% (-1.4% to +0.5%) |
| Windows: Watch-mode start | 2,569ms | 524ms | 524ms | -79.6% (-79.7% to -79.5%) | +0.1% (-0.5% to +0.6%) |
| macOS: Warm start | 118ms | 88ms | 88ms | -25.1% (-26.4% to -23.6%) | +0.1% (-1.8% to +1.9%) |
| Linux: Warm start | 636ms | 353ms | 353ms | -44.5% (-45.3% to -44.1%) | -0.1% (-1.8% to +1.1%) |
| Windows: Warm start | 355ms | 201ms | 201ms | -43.4% (-43.7% to -42.7%) | +0.0% (-0.9% to +0.7%) |

## Changelog

```
 - **[Performance]**: metro-file-map: Start watching while the cache is read
```

## Test plan

New unit tests. Benchmark as in #2104, with 22 rounds on macOS.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant