Skip to content

[Perf]: identical SplitChunks work and ChunkSplit mutations repeat on every hot rebuild #14770

Description

@matthewdavis-oai

Summary

After fixing a false-negative in Rspack's code-splitting cache, a visible, constant-length leaf.tsx edit in a large Rsbuild/React Router app consistently reuses the chunk graph but still executes the full SplitChunks pass. The client graph has roughly 408 route entries / 5,452 chunks; the edited module builds in 4-5 ms, while SplitChunks costs 0.81-1.40 s on each hot rebuild.

This is distinct from the existing leaf/chunk-graph report #14768 and from HMR process_assets #14767: those address earlier/later stages. There is currently no open SplitChunks-incremental issue or PR.

Attribution

Four consecutive hot edits in the post-cache-fix native trace:

                                  edit 1   edit 2   edit 3   edit 4
code_splitting (client, reuse)       26ms     29ms     28ms     21ms
SplitChunks                         1.15s    0.85s    0.81s    1.40s
root.css loader chain               1.13s    1.12s    0.93s    1.00s
leaf.tsx                               ~5ms     ~5ms     ~5ms     ~5ms

Each cache decision sees seven mutations/five affected modules and unchanged topology. SplitChunks' two prepare_module_group_map spans account for only about 80-190 ms combined; most of the pass is repeated full-graph grouping/application work. The browser run completes true HMRs with no CSS requests or reloads. The CSS row is the complete loader chain and is a separate opportunity, not time attributed to SplitChunks.

Artifacts:

trace:       hmr-codesplit-focused-20260713.jsonl
sha256:      2f2ceb002d18fc6d8dbb6084a3aae22eee1c836f94dd541de70a90ae5e51b7dc
HMR result:  rspack-codesplit-candidate-focused-result.json
sha256:      edbbf933c8270778849a35238505f3720199bee4a3e0050e9fc22df8a8ec874f
cache patch: rspack-codesplit-outgoing-cache-fix.patch
sha256:      c0ccae88e9588754089a71d4aa7a8136f0592005f89f6234302694af847f01b1
native:      rspack.linux-x64-gnu.node
sha256:      43aeb4bb914277fba080227d85de59fd45c73f329b393ef05be87d81e3ff2418

Why it repeats

MemoryCache::after_build_chunk_graph snapshots BuildChunkGraphArtifact before OptimizeChunksPass. That is the correct input for the next code-splitting decision, but a cache hit consequently starts SplitChunks from the unsplit graph every time. SplitChunksPlugin::inner_impl recalculates sizes/membership, prepares both default groups, moves modules, creates/reuses chunks, and reapplies relationships across the graph.

The repeated pass also unconditionally records Mutation::ChunkSplit { from, to } for every split (rspack_plugin_split_chunks/src/plugin/chunk.rs). Mutations::get_affected_chunks_with_chunk_graph subsequently marks both chunks affected, so recreating identical splits expands the incremental set for runtime requirements, hashing, and assets. The same trace spends another 0.68-0.95 s mostly idle in process_chunks_runtime_requirements. Reusing a verified post-optimize artifact could therefore save downstream work as well as the SplitChunks pass itself.

The app uses ordinary Rsbuild dev splitChunks: {}: Rspack's native default/defaultVendors async groups, usedExports: false, minSize: 10000, and no function-valued filters or names. Disabling sharing/splitting is not a viable workaround; a separate 416-entry browser topology matrix substantially inflated initial output and slowed rebuilds.

Proposed incremental boundary

Keep the existing pre-split snapshot and additionally retain an in-memory post-optimize chunks snapshot, captured immediately after the optimize pass and before optimizeChunkModules, runtime requirements, or asset emission. On a verified code-splitting cache hit, restore that snapshot and skip the pass only if all relevant SplitChunks inputs are unchanged; otherwise use the existing path and refresh it.

Do not reuse the previous final BuildChunkGraphArtifact: later stages connect runtime modules and populate chunk IDs/files/render state, which can introduce stale or duplicate runtime/emission state. Snapshot cloning overhead should be measured; an immutable/delta representation could avoid another large graph clone.

The conservative reuse predicate should require:

  • unchanged entry/group/chunk topology and a successful code-splitting reuse decision;
  • unchanged membership and per-source-type sizes for affected modules, including extracted CSS and size-threshold inputs;
  • unchanged module type/layer/name-for-condition/chunk-condition behavior and used-export/runtime partition when enabled;
  • bailout for function-valued chunks, cache-group test/name/layer, or other non-incremental optimize-chunk plugins.

This can be automatic for safe built-in configurations and needs no user-facing knob.

Suggested regressions

Cover (1) repeated visible leaf edits in a multi-entry async-sharing fixture, asserting stable topology and correct hot updates; (2) JS and extracted-CSS size changes crossing split thresholds; (3) add/remove/retarget of an async dependency; (4) usedExports and function-valued selectors changing results; and (5) successive HMRs with no stale runtime modules/chunk files/manifests. Each invalidating case must fall back to the existing full optimize pass.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions