Skip to content

perf: walk the file system off the thread webpack builds on - #357

Merged
alexander-akait merged 1 commit into
mainfrom
perf/nothing-sync-on-the-build-thread
Sep 14, 2026
Merged

alexander-akait merged 1 commit into
mainfrom
perf/nothing-sync-on-the-build-thread

Conversation

@alexander-akait

Copy link
Copy Markdown
Member

Summary

collectFromFileSystem walked the whole tree with globSync from inside the compilation hook, so every compilation stopped the loop for as long as the walk took — and the walk is made again on every rebuild. canWatch read each candidate directory with readdirSync, and parseFoldersToGlobs stated each pattern; both sit in places that can wait. All three are asynchronous now.

Measured over a generated 1000-file tree:

Loop blocked Wall
globSync 6.0 ms 6.0 ms
glob 0.3 ms 6.4 ms

End to end, the longest event-loop stall during a watch rebuild, nine rebuilds per run, two runs of each arm:

Stalls (ms) Median
before 9.5 9.9 10.0 10.1 10.2 10.5 15.1 15.1 15.2 10.2
after 4.5 5.0 5.2 5.3 5.8 6.0 6.2 7.8 8.5 5.8

A full build shows no difference — webpack's own work is the longest stall there — so the win is in rebuilds, which is where the walk is repeated and webpack has little to do.

Two compilers asking for the same walk at the same moment now share it rather than reading the tree twice. That also keeps multi-compiler's run sharing working: the walks used to finish in lockstep because they were synchronous, and without sharing the second compiler arrived after the first check's run had already settled — the existing test caught it.

For scale, the two small ones: readdirSync is 0.011 ms a call and statSync 0.002 ms, so they are converted because they cost nothing to convert, not because they showed up.

What stays synchronous is TypeScript's. Its host is called for a source file and must return one, so versionOf and the eight ts.sys calls have nothing to await into — and since #343 that work is on a worker, so it is not this loop. There is a comment saying so now.

What kind of change does this PR introduce?

perf.

Did you add tests for your changes?

No new ones — this changes no behaviour, and the existing suite covers it, including the multi-compiler case that failed on the first attempt. test/utils.test.js is updated for the now-async parseFoldersToGlobs.

Does this PR introduce a breaking change?

parseFoldersToGlobs is async now; it is not exported from the package entry.

If relevant, what needs to be documented once your changes are merged or what have you already documented?

n/a.

Use of AI

AI was used. It was asked to take the synchronous calls off the build's thread, measured each one first to decide which were worth converting, made the change, and measured the rebuild stall before and after in two paired runs.

🤖 Generated with Claude Code

https://claude.ai/code/session_01GzZci4NQeiqwdrVfd7dGXy


Generated by Claude Code

`collectFromFileSystem` walked the whole tree with `globSync` from inside
the `compilation` hook, so every compilation stopped the loop for as long
as the walk took — 6.0 ms over a thousand files, and the walk is made
again on every rebuild. `canWatch` read each candidate directory with
`readdirSync`, and `parseFoldersToGlobs` stated each pattern, both from
places that can wait.

All three are asynchronous now. Two compilers asking for the same walk at
the same moment share it rather than reading the tree twice, which is
also what leaves their checks a run to share.

Over a thousand files the longest stall in a watch rebuild falls from a
median of 10.2 ms to 5.8 ms, measured over nine rebuilds twice.

What stays synchronous is TypeScript's: its host is called for a source
file and waits for one, so `versionOf` and `ts.sys` have nothing to await
into — and since the check moved to a worker that is not this loop.
@alexander-akait
alexander-akait merged commit b41205b into main Sep 14, 2026
13 checks passed
@alexander-akait
alexander-akait deleted the perf/nothing-sync-on-the-build-thread branch September 14, 2026 13:07
This was referenced Sep 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant