perf(core): reuse chunk graph with stable async outgoings#14772
perf(core): reuse chunk graph with stable async outgoings#14772matthewdavis-oai wants to merge 2 commits into
Conversation
There was a problem hiding this comment.
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_maptopub(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.
| 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); | ||
|
|
Merging this PR will not alter performance
Comparing Footnotes
|
Summary
Fixes #14769.
BuildChunkGraphArtifact::can_skip_rebuilding_legacycurrently flattensall_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 andsource_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_corerustfmt --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 containsafter:stable, the ordered sync imports/side effect, andnext.bundle.js)git diff --checkThe focused data, standalone repros, and patch hashes are in the HMR handoff gist (available privately).