Skip to content

perf(core): reuse chunk graph with stable async outgoings#14772

Open
matthewdavis-oai wants to merge 2 commits into
web-infra-dev:mainfrom
matthewdavis-oai:matthewdavis-oai/hmr-outgoing-cache
Open

perf(core): reuse chunk graph with stable async outgoings#14772
matthewdavis-oai wants to merge 2 commits into
web-infra-dev:mainfrom
matthewdavis-oai:matthewdavis-oai/hmr-outgoing-cache

Conversation

@matthewdavis-oai

@matthewdavis-oai matthewdavis-oai commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #14769.

BuildChunkGraphArtifact::can_skip_rebuilding_legacy currently flattens all_dependencies() and compares the result with the cached root block. Async-block outgoings and dependency-creation order can consequently turn an unchanged module into a chunk-graph cache miss on every HMR.

This change mirrors CodeSplitter::prepare's module/context/weak filtering and source_order, compares the root and each async block independently, checks stable block/group options, and conservatively bails for nested blocks. The watch regression covers an unchanged non-leaf/re-export/async edge followed by a real async chunk-name change.

In the traced 408-entry React Router app, every hot edit previously logged a root-route outgoing mismatch and spent 1.6--2.8 s rebuilding the chunk graph. With the fix, all measured edits reuse code splitting (21--29 ms); a full browser run completed 10/10 true HMR updates with zero CSS requests/reloads and improved visible p50 from 7.37 s to 5.97 s. Remaining SplitChunks/runtime work is tracked separately in #14770.

Validation

  • cargo check --locked --offline -p rspack_core
  • rustfmt --edition 2024 --check crates/rspack_core/src/artifacts/build_chunk_graph_artifact.rs crates/rspack_core/src/compilation/build_chunk_graph/code_splitter.rs
  • ./node_modules/.bin/rstest run Incremental-watch.test.js --project base -t 'async-block-outgoings' --pool.maxWorkers 1 --maxConcurrency 1 --reporter verbose (3/3 watch steps passed; final output contains after:stable, the ordered sync imports/side effect, and next.bundle.js)
  • JS syntax and git diff --check
  • optimized native 250-entry topology matrix: unchanged leaf rounds reuse; async chunk rename, retarget, and add-edge rounds rebuild; emitted updates contain the changed value
  • extracted-CSS/native-CSS and real Tailwind/persistent-cache/SplitChunks matrices: JS-only updates contain no CSS update, stylesheet edits update correctly
  • full browser harness: 10/10 true HMR, one web/node generation per edit, zero measured CSS requests or navigation

The focused data, standalone repros, and patch hashes are in the HMR handoff gist (available privately).

@matthewdavis-oai matthewdavis-oai marked this pull request as ready for review July 13, 2026 21:42
Copilot AI review requested due to automatic review settings July 13, 2026 21:42

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 incremental chunk-graph reuse during HMR by making BuildChunkGraphArtifact::can_skip_rebuilding_legacy compare dependency outgoings using the same per-block (root vs async block) semantics and ordering rules as CodeSplitter::prepare, avoiding false cache misses caused by async-block outgoings and dependency creation order.

Changes:

  • Update the legacy chunk-graph reuse predicate to (1) validate async block identity/options stability and (2) compare outgoings per root/async block with source_order-aligned ordering and filtering.
  • Expose CodeSplitter::prepared_blocks_map to pub(crate) so the reuse predicate can compare current vs cached async blocks.
  • Add a new watch regression case covering “unchanged leaf edit should reuse” vs “async chunk-name change must rebuild”.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
crates/rspack_core/src/artifacts/build_chunk_graph_artifact.rs Reworks the legacy reuse predicate to compare root + async block outgoings independently and validate async-block option stability.
crates/rspack_core/src/compilation/build_chunk_graph/code_splitter.rs Makes prepared_blocks_map accessible within the crate to support reuse checks.
tests/rspack-test/watchCases/build-chunk-graph/async-block-outgoings/test.config.js Asserts (via stats/logging) that leaf-only edits reuse the chunk graph and async chunk-name changes rebuild.
tests/rspack-test/watchCases/build-chunk-graph/async-block-outgoings/rspack.config.js Configures incremental buildChunkGraph + verbose logging for the regression fixture.
tests/rspack-test/watchCases/build-chunk-graph/async-block-outgoings/0/index.js Initial entry module for the watch fixture.
tests/rspack-test/watchCases/build-chunk-graph/async-block-outgoings/0/route.js Initial route module with sync imports, re-export, and a stable async import.
tests/rspack-test/watchCases/build-chunk-graph/async-block-outgoings/0/leaf.js Initial leaf module content used to trigger a “no topology change” edit.
tests/rspack-test/watchCases/build-chunk-graph/async-block-outgoings/0/terminal.js Stable dependency for leaf content.
tests/rspack-test/watchCases/build-chunk-graph/async-block-outgoings/0/sync-first.js Sync dependency used to validate ordered sync import handling.
tests/rspack-test/watchCases/build-chunk-graph/async-block-outgoings/0/sync-second.js Sync dependency used to validate ordered sync import handling.
tests/rspack-test/watchCases/build-chunk-graph/async-block-outgoings/0/side-effect.js Side-effect module used to validate side-effect filtering behavior.
tests/rspack-test/watchCases/build-chunk-graph/async-block-outgoings/0/lazy.js Async target module for the route’s dynamic import.
tests/rspack-test/watchCases/build-chunk-graph/async-block-outgoings/1/leaf.js Step 1 update: leaf-only edit intended to reuse the chunk graph.
tests/rspack-test/watchCases/build-chunk-graph/async-block-outgoings/2/route.js Step 2 update: changes webpackChunkName to force a rebuild.

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

Comment on lines +145 to +164
let mut ordered_dependencies = vec![];
let mut unordered_dependencies = vec![];
for dep_id in module_graph_module.all_dependencies() {
let dependency = module_graph.dependency_by_id(dep_id);
let module_dependency = dependency.as_module_dependency();
if (module_dependency.is_none() && dependency.as_context_dependency().is_none())
|| module_dependency.is_some_and(|module_dep| module_dep.weak())
|| module_graph.connection_by_dependency_id(dep_id).is_none()
{
continue;
}

if let Some(source_order) = dependency.source_order() {
ordered_dependencies.push((source_order, *dep_id));
} else {
unordered_dependencies.push(*dep_id);
}
}
ordered_dependencies.sort_by_key(|(source_order, _)| *source_order);

@codspeed-hq

codspeed-hq Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 43 untouched benchmarks
⏩ 47 skipped benchmarks1


Comparing matthewdavis-oai:matthewdavis-oai/hmr-outgoing-cache (3b1bc3b) with main (cfa680e)

Open in CodSpeed

Footnotes

  1. 47 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.

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]: unchanged async outgoings falsely invalidate the chunk-graph cache during HMR

3 participants