Skip to content

fix: narrow purge for watched directories in NodeWatchFileSystem#21020

Merged
alexander-akait merged 6 commits into
mainfrom
claude/fix-issue-13636-ts24Z
May 22, 2026
Merged

fix: narrow purge for watched directories in NodeWatchFileSystem#21020
alexander-akait merged 6 commits into
mainfrom
claude/fix-issue-13636-ts24Z

Conversation

@alexander-akait

Copy link
Copy Markdown
Member

Watchpack reports a watched directory (a context dependency) in changes
whenever its contents change, alongside the individual file events.
inputFileSystem.purge(dir) uses key.startsWith(dir) internally, so
calling it on the directory wiped the stat cache of every file inside,
even though only file-level events actually invalidate file stats.
ContextModuleFactory.resolveDependencies then re-stat-ed every file in
the tree on every incremental rebuild.

For directories we explicitly watch, invalidate only the directory's own
readdir entry; file-level changes in the same aggregated event continue
to purge file stats and the parent readdir (via purgeParent) as
before, so adds, removes, modifies and nested changes are still detected.

Single-file rebuilds on a local 4000-file context drop from ~370 ms to
~225 ms (~40%).

Closes #13636

Watchpack reports a watched directory (a context dependency) in `changes`
whenever its contents change, alongside the individual file events.
`inputFileSystem.purge(dir)` uses `key.startsWith(dir)` internally, so
calling it on the directory wiped the stat cache of every file inside,
even though only file-level events actually invalidate file stats.
`ContextModuleFactory.resolveDependencies` then re-stat-ed every file in
the tree on every incremental rebuild.

For directories we explicitly watch, invalidate only the directory's own
`readdir` entry; file-level changes in the same aggregated event continue
to purge file stats and the parent `readdir` (via `purgeParent`) as
before, so adds, removes, modifies and nested changes are still detected.

Single-file rebuilds on a local 4000-file context drop from ~370 ms to
~225 ms (~40%).

Closes #13636
…actory

`ContextModuleFactory.resolveDependencies` previously fired the
`alternativeRequests` hook once per file with a single-item array, paying
per-call overhead (closure allocation, `compiler.resolverFactory.get(...)`
lookup, intermediate array allocations inside `RequireContextPlugin`'s
tap) for every file in the context. Collect all matched files per
directory walk and invoke the hook once with the full batch; the existing
tap already iterates the items array so the output is unchanged.

Trims a further ~15 ms from steady-state rebuilds on a 4000-file
`require.context` workload.
…TED_ANY

Replace the `/** @type {EXPECTED_ANY} */ (fs)._readdirBackend` cast in
`NodeWatchFileSystem`'s narrow-purge path with a local `CacheBackend`
typedef and an `InputFileSystemWithBackends` augmented file-system type,
so the private backend slot we read off enhanced-resolve's
`CachedInputFileSystem` is described precisely instead of widened to
`any`.

No behavior change.
…o _readdirBackend

Replace the private `_readdirBackend.purge(item)` access with the
public unified API added in enhanced-resolve@5.22.0:

    fs.purge(item, { exact: true })

This invalidates only the exact key across every backend (stat,
lstat, readdir, readlink, realpath, readFile, readJson) without
removing cached entries for descendants — the same semantics the
previous _readdirBackend probe was approximating, but expressed as
the consumer-facing API rather than a private slot.

Drops the CacheBackend / InputFileSystemWithBackends JSDoc typedefs
that only existed to type the private access, and bumps the
enhanced-resolve peer to ^5.22.0.

Performance characteristic is unchanged: median single-file rebuild
on a 4000-file watched context is ~650 ms vs ~1260 ms without
narrowing (≈49% local reproduction).
Lockfile update following the package.json bump to ^5.22.0, which
introduced the { exact: true } option on CachedInputFileSystem#purge
consumed by NodeWatchFileSystem.
Copilot AI review requested due to automatic review settings May 22, 2026 09:53
@changeset-bot

changeset-bot Bot commented May 22, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: e8cb5a2

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
webpack Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actions Bot commented May 22, 2026

Copy link
Copy Markdown
Contributor

This PR is packaged and the instant preview is available (a086147).

Install it locally:

  • npm
npm i -D webpack@https://pkg.pr.new/webpack@a086147
  • yarn
yarn add -D webpack@https://pkg.pr.new/webpack@a086147
  • pnpm
pnpm add -D webpack@https://pkg.pr.new/webpack@a086147

@codecov

codecov Bot commented May 22, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 91.57%. Comparing base (c755747) to head (e8cb5a2).
⚠️ Report is 2 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main   #21020   +/-   ##
=======================================
  Coverage   91.57%   91.57%           
=======================================
  Files         573      573           
  Lines       59460    59465    +5     
  Branches    16054    16058    +4     
=======================================
+ Hits        54449    54458    +9     
+ Misses       5011     5007    -4     
Flag Coverage Δ
integration 89.49% <100.00%> (+<0.01%) ⬆️
test262 45.31% <51.51%> (+<0.01%) ⬆️
unit 37.95% <51.51%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Improves watch-mode incremental rebuild performance for large context dependencies by avoiding overly broad filesystem cache invalidation and reducing per-file hook-call overhead in context dependency resolution.

Changes:

  • Narrow inputFileSystem.purge() invalidation for explicitly watched context directories by using { exact: true } (enhanced-resolve ≥ 5.22.0) in NodeWatchFileSystem.
  • Batch ContextModuleFactory’s alternativeRequests hook invocation per directory instead of per file to reduce overhead.
  • Bump enhanced-resolve to ^5.22.0 and update filesystem purge typings/documentation accordingly; add changesets for release notes.

Reviewed changes

Copilot reviewed 6 out of 8 changed files in this pull request and generated no comments.

Show a summary per file
File Description
lib/node/NodeWatchFileSystem.js Purges watched directories with { exact: true } to avoid wiping descendant stat cache on directory change events.
lib/ContextModuleFactory.js Batches alternativeRequests hook calls per directory to cut per-file overhead during context resolution.
lib/util/fs.js Extends Purge typedef docs/signature to include optional { exact?: boolean }.
package.json Updates enhanced-resolve dependency to ^5.22.0.
yarn.lock Locks enhanced-resolve to 5.22.0.
types.d.ts Updates generated InputFileSystem.purge type signature to accept optional { exact?: boolean }.
.changeset/watch-narrow-purge-context-dir.md Adds changeset entry for the watch-mode purge narrowing performance fix.
.changeset/batch-alternative-requests.md Adds changeset entry for batching alternativeRequests performance improvement.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

enhanced-resolve 5.22.0 introduced a CompiledAliasOptions typedef
(in AliasUtils.js) referenced transitively via TsconfigPathsData.alias.
yarn fix:special picks it up; CI's lint:special job requires the
regenerated file to be committed.
@codspeed-hq

codspeed-hq Bot commented May 22, 2026

Copy link
Copy Markdown

Merging this PR will improve performance by 20.42%

⚡ 11 improved benchmarks
❌ 2 regressed benchmarks
✅ 131 untouched benchmarks
⏩ 72 skipped benchmarks1

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Memory benchmark "asset-modules-bytes", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 136.7 KB 322.9 KB -57.68%
Memory benchmark "context-commonjs", scenario '{"name":"mode-development","mode":"development"}' 1,313.2 KB 866 KB +51.64%
Memory benchmark "context-esm", scenario '{"name":"mode-production","mode":"production"}' 9.3 MB 7.3 MB +27.99%
Memory benchmark "many-modules-esm", scenario '{"name":"mode-development","mode":"development"}' 1.3 MB 1.1 MB +23.59%
Memory benchmark "future-defaults", scenario '{"name":"mode-production","mode":"production"}' 11.8 MB 7.1 MB +65.02%
Memory benchmark "css-modules", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 504.4 KB 415.6 KB +21.38%
Memory benchmark "devtool-eval-source-map", scenario '{"name":"mode-production","mode":"production"}' 7.9 MB 6.4 MB +24.47%
Memory benchmark "asset-modules-resource", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 352.5 KB 208.6 KB +68.94%
Memory benchmark "side-effects-reexport", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 409.2 KB 1,409.4 KB -70.96%
Memory benchmark "many-modules-esm", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 288.8 KB 144 KB ×2
Memory benchmark "many-chunks-esm", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 246.4 KB 170.8 KB +44.22%
Memory benchmark "many-chunks-commonjs", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 252.1 KB 170.2 KB +48.15%
Memory benchmark "cache-filesystem", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 351.1 KB 166.8 KB ×2.1

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing claude/fix-issue-13636-ts24Z (e8cb5a2) with main (c755747)

Open in CodSpeed

Footnotes

  1. 72 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@github-actions

Copy link
Copy Markdown
Contributor

Types Coverage

Coverage after merging claude/fix-issue-13636-ts24Z into main will be
98.96%
Coverage Report
FileStmtsBranchesFuncsLinesUncovered Lines
bin
   webpack.js98.77%100%100%98.77%91
examples
   build-common.js100%100%100%100%
   buildAll.js100%100%100%100%
   examples.js100%100%100%100%
   template-common.js98.21%100%100%98.21%72
examples/custom-javascript-parser
   test.filter.js100%100%100%100%
examples/custom-javascript-parser/internals
   acorn-parse.js100%100%100%100%
   meriyah-parse.js100%100%100%100%
   oxc-parse.js91.30%100%100%91.30%140, 142–143, 145, 147, 153–154, 161, 168, 90
examples/markdown
   webpack.config.mjs100%100%100%100%
examples/typescript
   test.filter.js100%100%100%100%
examples/typescript-non-erasable
   test.filter.js50%100%100%50%5
examples/virtual-modules
   test.filter.js100%100%100%100%
examples/wasm-bindgen-esm
   test.filter.js100%100%100%100%
examples/wasm-complex
   test.filter.js100%100%100%100%
examples/wasm-simple
   test.filter.js100%100%100%100%
examples/wasm-simple-source-phase
   test.filter.js100%100%100%100%
lib
   APIPlugin.js100%100%100%100%
   AsyncDependenciesBlock.js100%100%100%100%
   AutomaticPrefetchPlugin.js100%100%100%100%
   BannerPlugin.js100%100%100%100%
   Cache.js98.21%100%100%98.21%101
   CacheFacade.js100%100%100%100%
   Chunk.js99.72%100%100%99.72%39
   ChunkGraph.js100%100%100%100%
   ChunkGroup.js100%100%100%100%
   ChunkTemplate.js100%100%100%100%
   CleanPlugin.js99.15%100%100%99.15%206, 226
   CodeGenerationResults.js100%100%100%100%
   CompatibilityPlugin.js100%100%100%100%
   Compilation.js98.45%100%100%98.45%1572, 1868, 1875, 1883, 1905, 2801, 3226, 3888, 3917, 3970–3971, 3975, 3980, 3996–3997, 4011–4012, 4017–4018, 4495, 4521, 511, 516, 5229, 5261, 5278, 5294, 5310, 5325, 5350–5351, 5353, 5681, 5686, 5692, 5695, 5707, 5709, 5713, 5729, 5744, 5776, 5830, 5854, 5968, 730–731
   Compiler.js99.55%100%100%99.55%1116–1117, 1125
   ConcatenationScope.js98.59%100%100%98.59%189
   ConditionalInitFragment.js100%100%100%100%
   ConstPlugin.js100%100%100%100%
   ContextExclusionPlugin.js100%100%100%100%
   ContextModule.js100%100%100%100%
   ContextModuleFactory.js97.40%100%100%97.40%258, 395, 418, 420, 424, 433–434
   ContextReplacementPlugin.js100%100%100%100%
   DefinePlugin.js98.92%100%100%98.92%158–159, 175, 194, 268
   DependenciesBlock.js100%100%100%100%
   Dependency.js98.20%100%100%98.20%379, 425
   DependencyTemplate.js100%100%100%100%
   DependencyTemplates.js100%100%100%100%
   DotenvPlugin.js98.41%100%100%98.41%378, 391–392
   DynamicEntryPlugin.js100%100%100%100%
   EntryOptionPlugin.js100%100%100%100%
   EntryPlugin.js100%100%100%100%
   Entrypoint.js100%100%100%100%
   EnvironmentPlugin.js97.14%100%100%97.14%49
   ErrorHelpers.js100%100%100%100%
   EvalDevToolModulePlugin.js100%100%100%100%
   EvalSourceMapDevToolPlugin.js100%100%100%100%
   ExportsInfo.js100%100%100%100%
   ExportsInfoApiPlugin.js100%100%100%100%
   ExternalModule.js98.97%100%100%98.97%425–429, 577
   ExternalModuleFactoryPlugin.js100%100%100%100%
   ExternalsPlugin.js100%100%100%100%
   FileSystemInfo.js99.50%100%100%99.50%182, 2252–2253, 2256, 2267, 2278, 2289, 278, 3694, 3709, 3733
   FlagAllModulesAsUsedPlugin.js100%100%100%100%
   FlagDependencyExportsPlugin.js98.74%100%100%98.74%399, 401, 405
   FlagDependencyUsagePlugin.js100%100%100%100%
   FlagEntryExportAsUsedPlugin.js100%100%100%100%
   Generator.js100%100%100%100%
   HotModuleReplacementPlugin.js100%100%100%100%
   HotUpdateChunk.js100%100%100%100%
   IgnorePlugin.js100%100%100%100%
   IgnoreWarningsPlugin.js100%100%100%100%
   InitFragment.js100%100%100%100%
   JavascriptMetaInfoPlugin.js100%100%100%100%
   LibraryTemplatePlugin.js100%100%100%100%
   LoaderOptionsPlugin.js100%100%100%100%
   LoaderTargetPlugin.js100%100%100%100%
   MainTemplate.js100%100%100%100%
   ManifestPlugin.js100%100%100%100%
   Module.js98.50%100%100%98.50%1305, 1310, 1371, 1385, 1447, 1456
   ModuleFactory.js100%100%100%100%
   ModuleFilenameHelpers.js98.85%100%100%98.85%106, 108
   ModuleGraph.js99.73%100%100%99.73%1004
   ModuleGraphConnection.js100%100%100%100%
   ModuleInfoHeaderPlugin.js100%100%100%100%
   ModuleNotFoundError.js100%100%100%100%
   ModuleProfile.js100%100%100%100%
   ModuleSourceTypeConstants.js100%100%100%100%
   ModuleTemplate.js100%100%100%100%
   ModuleTypeConstants.js100%100%100%100%
   MultiCompiler.js99.69%100%100%99.69%645
   MultiStats.js100%100%100%100%
   MultiWatching.js100%100%100%100%
   NoEmitOnErrorsPlugin.js100%100%100%100%
   NodeStuffPlugin.js100%100%100%100%
   NormalModule.js98.11%100%100%98.11%1211, 1214, 1231, 1248, 1489, 1523, 1539, 1626, 2249, 2254–2264, 569
   NormalModuleFactory.js99.47%100%100%99.47%1083, 1392, 486, 498
   NormalModuleReplacementPlugin.js100%100%100%100%
   NullFactory.js100%100%100%100%
   OptimizationStages.js100%100%100%100%
   OptionsApply.js100%100%100%100%
   Parser.js100%100%100%100%
   PlatformPlugin.js100%100%100%100%
   PrefetchPlugin.js100%100%100%100%
   ProgressPlugin.js98.85%100%100%98.85%519–520, 525, 527, 591
   ProvidePlugin.js100%100%100%100%
   RawModule.js100%100%100%100%
   RecordIdsPlugin.js100%100%100%100%
   RequestShortener.js100%100%100%100%
   ResolverFactory.js100%100%100%100%
   RuntimeGlobals.js100%100%100%100%
   RuntimeModule.js100%100%100%100%
   RuntimePlugin.js100%100%100%100%
   RuntimeTemplate.js100%100%100%100%
   SelfModuleFactory.js100%100%100%100%
   SingleEntryPlugin.js100%100%100%100%
   SourceMapDevToolModuleOptionsPlugin.js100%100%100%100%
   SourceMapDevToolPlugin.js98.59%100%100%98.59%218, 222, 224, 398, 409, 811
   Stats.js100%100%100%100%
   Template.js100%100%100%100%
   TemplatedPathPlugin.js98.86%100%100%98.86%136–137
   UseStrictPlugin.js100%100%100%100%
   WarnCaseSensitiveModulesPlugin.js100%100%100%100%
   WarnDeprecatedOptionPlugin.js100%100%100%100%
   WarnNoModeSetPlugin.js100%100%100%100%
   WatchIgnorePlugin.js100%100%100%100%
   Watching.js100%100%100%100%
   WebpackError.js100%100%100%100%
   WebpackIsIncludedPlugin.js100%100%100%100%
   WebpackOptionsApply.js100%100%100%100%
   WebpackOptionsDefaulter.js100%100%100%100%
   buildChunkGraph.js99.87%100%100%99.87%325
   cli.js98.46%100%100%98.46%10, 119, 471, 503, 545, 815
   index.js99.72%100%100%99.72%165
   validateSchema.js94.67%100%100%94.67%100, 87, 89, 98
   webpack.js96.33%100%100%96.33%10, 198, 220, 222
lib/asset
   AssetBytesGenerator.js100%100%100%100%
   AssetBytesParser.js100%100%100%100%
   AssetGenerator.js100%100%100%100%
   AssetModulesPlugin.js97.32%100%100%97.32%283, 307, 310, 36, 362, 41
   AssetParser.js100%100%100%100%
   AssetSourceGenerator.js100%100%100%100%
   AssetSourceParser.js100%100%100%100%
   RawDataUrlModule.js100%100%100%100%
lib/async-modules
   AsyncModuleHelpers.js100%100%100%100%
   AwaitDependenciesInitFragment.js100%100%100%100%
   InferAsyncModulesPlugin.js100%100%100%100%
lib/cache
   AddBuildDependenciesPlugin.js100%100%100%100%
   AddManagedPathsPlugin.js100%100%100%100%
   IdleFileCachePlugin.js97.92%100%100%97.92%71, 83, 91
   MemoryCachePlugin.js95.83%100%100%95.83%33
   MemoryWithGcCachePlugin.js93.15%100%100%93.15%106, 113–114, 122, 89
   PackFileCacheStrategy.js96.40%100%100%96.40%1250, 1350, 1354, 1416, 628, 647, 657–659, 661, 677–678, 683, 686, 688, 693, 698, 722, 728, 762, 768, 774, 779, 790, 799, 804–805, 807, 824, 830–831, 833
   ResolverCachePlugin.js100%100%100%100%
   getLazyHashedEtag.js100%100%100%100%
   mergeEtags.js100%100%100%100%
lib/config
   browserslistTargetHandler.js100%100%100%100%
   defaults.js99.29%100%100%99.29%1411–1413, 1421, 271, 274, 279, 283
   normalization.js99%100%100%99%191–192, 258, 273
   target.js100%100%100%100%
lib/container
   ContainerEntryDependency.js100%100%100%100%
   ContainerEntryModule.js100%100%100%100%
   ContainerEntryModuleFactory.js100%100%100%100%
   ContainerExposedDependency.js100%100%100%100%
   ContainerPlugin.js100%100%100%100%
   ContainerReferencePlugin.js100%100%100%100%
   FallbackDependency.js100%100%100%100%
   

@alexander-akait alexander-akait merged commit a086147 into main May 22, 2026
61 checks passed
@alexander-akait alexander-akait deleted the claude/fix-issue-13636-ts24Z branch May 22, 2026 11:49
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.

perf problems with require.context

2 participants