|
| 1 | +--- |
| 2 | +name: rspack-split-chunks |
| 3 | +description: >- |
| 4 | + Diagnose and optimize Rspack `optimization.splitChunks` configuration. Use |
| 5 | + this when a user wants better production chunking, safer `chunks: "all"` |
| 6 | + defaults, fewer duplicated modules, better long-term caching, `cacheGroups` |
| 7 | + design help, `maxSize` tuning, or debugging over-fetch caused by `name` and |
| 8 | + forced chunk merging. |
| 9 | +--- |
| 10 | + |
| 11 | +# Rspack SplitChunks Optimization |
| 12 | + |
| 13 | +Use this skill when the task is to recommend, review, or debug `optimization.splitChunks`. If you are using ESM library, it's not the same algorithm of this skill. |
| 14 | + |
| 15 | +## Default stance |
| 16 | + |
| 17 | +- Distinguish repo defaults from recommended production baselines. |
| 18 | +- Rspack's built-in default is `chunks: "async"`, but for most production web apps the best starting point is: |
| 19 | + |
| 20 | +```js |
| 21 | +optimization: { |
| 22 | + splitChunks: { |
| 23 | + chunks: "all", |
| 24 | + }, |
| 25 | +} |
| 26 | +``` |
| 27 | + |
| 28 | +- Keep the default cache groups unless there is a concrete reason to replace them. |
| 29 | +- Treat `name` as a graph-shaping option, not a cosmetic naming option. |
| 30 | +- Do not use `splitChunks` to reason about JavaScript execution order or tree shaking. For JS, chunk loading/execution order is preserved by the runtime dependency graph, and tree shaking is decided elsewhere. |
| 31 | + |
| 32 | +Read [`references/repo-behavior.md`](references/repo-behavior.md) when you need the source-backed rationale. |
| 33 | + |
| 34 | +## What To Optimize For |
| 35 | + |
| 36 | +First identify which problem the user actually has: |
| 37 | + |
| 38 | +- duplicated modules across entry or async boundaries |
| 39 | +- a route fetching a large shared chunk with mostly unused modules |
| 40 | +- too many tiny chunks |
| 41 | +- a vendor/common chunk that changes too often and hurts caching |
| 42 | +- an oversized async or initial chunk that should be subdivided |
| 43 | +- confusion about whether `splitChunks` affects runtime execution order |
| 44 | + |
| 45 | +Do not optimize all of these at once. Pick the primary goal and keep the rest as constraints. |
| 46 | + |
| 47 | +## Workflow |
| 48 | + |
| 49 | +### 1. Start from the safest production baseline |
| 50 | + |
| 51 | +Unless the user already has a measured problem that requires custom grouping, prefer: |
| 52 | + |
| 53 | +```js |
| 54 | +optimization: { |
| 55 | + splitChunks: { |
| 56 | + chunks: "all", |
| 57 | + }, |
| 58 | +} |
| 59 | +``` |
| 60 | + |
| 61 | +Why: |
| 62 | + |
| 63 | +- it lets splitChunks dedupe modules across both initial and async chunks |
| 64 | +- it still only loads chunks reachable from the current entry/runtime |
| 65 | +- it usually avoids loading unnecessary modules better than hand-written global vendor buckets |
| 66 | + |
| 67 | +If the existing config disables `default` or `defaultVendors`, assume that is suspicious until proven necessary. |
| 68 | + |
| 69 | +### 2. Audit the config for high-risk knobs |
| 70 | + |
| 71 | +Check these first: |
| 72 | + |
| 73 | +- fixed `name` |
| 74 | +- `cacheGroups.*.name` |
| 75 | +- `enforce: true` |
| 76 | +- disabled `default` / `defaultVendors` |
| 77 | +- broad `test: /node_modules/` rules combined with a single global `name` |
| 78 | +- `usedExports: false` |
| 79 | +- very small `minSize` |
| 80 | +- `maxSize` combined with manual global names |
| 81 | + |
| 82 | +### 3. Interpret `name` correctly |
| 83 | + |
| 84 | +Use this rule: |
| 85 | + |
| 86 | +- No `name`: splitChunks can keep different chunk combinations separate. |
| 87 | +- Same `name`: matching modules are merged into the same named split chunk candidate. |
| 88 | + |
| 89 | +That means a fixed `name: "vendors"` or `name: "common"` is often the real reason a page starts fetching modules from unrelated dependency chains. |
| 90 | + |
| 91 | +Prefer these alternatives before adding `name`: |
| 92 | + |
| 93 | +- keep `name` unset |
| 94 | +- use `idHint` if the goal is filename identity, not grouping identity |
| 95 | +- narrow the `test` so the cache group is smaller |
| 96 | +- split one broad cache group into several focused cache groups |
| 97 | +- rely on `maxSize` to subdivide a big chunk instead of forcing a global name |
| 98 | + |
| 99 | +Use a fixed `name` only when the user explicitly wants one shared asset across multiple entries/routes and accepts the extra coupling. |
| 100 | + |
| 101 | +### 4. Preserve the built-in cache groups by default |
| 102 | + |
| 103 | +Rspack's built-in production-oriented behavior depends heavily on these two groups: |
| 104 | + |
| 105 | +- `default`: extracts modules shared by at least 2 chunks and reuses existing chunks |
| 106 | +- `defaultVendors`: extracts `node_modules` modules and reuses existing chunks |
| 107 | + |
| 108 | +These defaults are usually the best balance between dedupe and "only fetch what this page needs". |
| 109 | + |
| 110 | +If you customize `cacheGroups`, do not casually replace these with one manually named vendor bucket. |
| 111 | + |
| 112 | +### 5. Use `chunks: "all"` without fear of breaking execution order |
| 113 | + |
| 114 | +When a module group is split out, Rspack connects the new chunk back to the original chunk groups. That preserves JavaScript loading semantics. |
| 115 | + |
| 116 | +So: |
| 117 | + |
| 118 | +- `splitChunks` changes chunk topology |
| 119 | +- the runtime still guarantees dependency loading/execution order |
| 120 | +- if execution order appears broken, look for other causes first |
| 121 | +- this statement is about JavaScript, not CSS order |
| 122 | + |
| 123 | +### 6. Use `maxSize` as a refinement tool |
| 124 | + |
| 125 | +Use `maxSize`, `maxAsyncSize`, or `maxInitialSize` when the problem is "this shared chunk is too large", not when the problem is "I need a stable vendor chunk name". |
| 126 | + |
| 127 | +Important behavior: |
| 128 | + |
| 129 | +- `maxSize` runs after a chunk already exists |
| 130 | +- the split is deterministic |
| 131 | +- modules are grouped by path-derived keys and split near low-similarity boundaries |
| 132 | +- similar file paths tend to stay together |
| 133 | + |
| 134 | +This is usually safer than forcing one giant named vendor chunk, because it keeps chunk graph semantics while subdividing hot spots. |
| 135 | + |
| 136 | +### 7. Use `usedExports` deliberately |
| 137 | + |
| 138 | +If the user has multiple runtimes/entries and wants leaner shared chunks per runtime, prefer keeping `usedExports` enabled. |
| 139 | + |
| 140 | +If they set `usedExports: false`, expect broader sharing and potentially larger common chunks. |
| 141 | + |
| 142 | +This is still not tree shaking. It only changes how splitChunks groups modules across runtimes. |
| 143 | + |
| 144 | +### 8. Treat `enforce: true` as an escape hatch |
| 145 | + |
| 146 | +`enforce: true` bypasses several normal guardrails. Use it only when the user intentionally wants a split regardless of `minSize`, `minChunks`, and request limits. |
| 147 | + |
| 148 | +If a config looks aggressive and hard to explain, check `enforce` before changing anything else. |
| 149 | + |
| 150 | +## Recommendations By Goal |
| 151 | + |
| 152 | +### Better default production chunking |
| 153 | + |
| 154 | +Recommend: |
| 155 | + |
| 156 | +```js |
| 157 | +optimization: { |
| 158 | + splitChunks: { |
| 159 | + chunks: "all", |
| 160 | + }, |
| 161 | +} |
| 162 | +``` |
| 163 | + |
| 164 | +Avoid: |
| 165 | + |
| 166 | +- disabling `default` |
| 167 | +- disabling `defaultVendors` |
| 168 | +- adding `name` before measuring a real problem |
| 169 | + |
| 170 | +### Avoid fetching non-essential modules |
| 171 | + |
| 172 | +Recommend: |
| 173 | + |
| 174 | +- remove fixed `name` |
| 175 | +- keep cache groups narrow |
| 176 | +- keep `chunks: "all"` if dedupe across initial chunks is still desired |
| 177 | +- inspect which routes now depend on a shared chunk after each change |
| 178 | + |
| 179 | +Avoid: |
| 180 | + |
| 181 | +```js |
| 182 | +cacheGroups: { |
| 183 | + vendors: { |
| 184 | + test: /[\\/]node_modules[\\/]/, |
| 185 | + chunks: "all", |
| 186 | + name: "vendors", |
| 187 | + enforce: true |
| 188 | + } |
| 189 | +} |
| 190 | +``` |
| 191 | + |
| 192 | +That pattern often creates one over-shared chunk that many pages must fetch. |
| 193 | + |
| 194 | +### Improve caching without over-merging |
| 195 | + |
| 196 | +Recommend: |
| 197 | + |
| 198 | +- keep `name` unset |
| 199 | +- use `idHint` |
| 200 | +- keep `chunkIds: "deterministic"` or other stable id strategies elsewhere in the config |
| 201 | +- split broad groups into smaller focused groups only when the package boundaries are stable and important |
| 202 | + |
| 203 | +Use a fixed `name` only if the user explicitly prefers cache reuse over route isolation. |
| 204 | + |
| 205 | +### Split a large shared chunk |
| 206 | + |
| 207 | +Recommend: |
| 208 | + |
| 209 | +```js |
| 210 | +optimization: { |
| 211 | + splitChunks: { |
| 212 | + chunks: "all", |
| 213 | + maxSize: 200000, |
| 214 | + }, |
| 215 | +} |
| 216 | +``` |
| 217 | + |
| 218 | +Then tune: |
| 219 | + |
| 220 | +- `maxAsyncSize` when async chunks are the pain point |
| 221 | +- `maxInitialSize` when first-load pressure matters more |
| 222 | +- `hidePathInfo` if generated part names should not leak path structure |
| 223 | + |
| 224 | +### Keep an intentionally shared chunk |
| 225 | + |
| 226 | +Recommend a named chunk only when the user says something like: |
| 227 | + |
| 228 | +- "all pages should share one React vendor asset" |
| 229 | +- "I want one framework chunk for cache reuse across routes" |
| 230 | + |
| 231 | +Even then, call out the tradeoff explicitly: |
| 232 | + |
| 233 | +- better cache hit rate |
| 234 | +- more coupling between routes |
| 235 | +- a page may fetch modules it does not execute immediately |
| 236 | + |
| 237 | +## Review Checklist |
| 238 | + |
| 239 | +When reviewing a user's config, explicitly answer: |
| 240 | + |
| 241 | +1. Is the goal dedupe, cache stability, request count, or route isolation? |
| 242 | +2. Is `chunks: "all"` a better baseline than the current config? |
| 243 | +3. Did `name` accidentally turn multiple candidates into one forced shared chunk? |
| 244 | +4. Were `default` or `defaultVendors` disabled without a strong reason? |
| 245 | +5. Would `idHint` satisfy the naming goal without changing grouping? |
| 246 | +6. Is `maxSize` a better fit than a broad manual vendor/common bucket? |
| 247 | +7. Does the result still keep each page fetching only reachable chunks? |
| 248 | + |
| 249 | +## Minimal stats setup |
| 250 | + |
| 251 | +When the task includes diagnosis, ask for or generate stats that expose chunk relations: |
| 252 | + |
| 253 | +```js |
| 254 | +stats: { |
| 255 | + chunks: true, |
| 256 | + chunkRelations: true, |
| 257 | + chunkOrigins: true, |
| 258 | + entrypoints: true, |
| 259 | + modules: false |
| 260 | +} |
| 261 | +``` |
| 262 | + |
| 263 | +Then compare: |
| 264 | + |
| 265 | +- which entrypoints reference which shared chunks |
| 266 | +- whether a change added a new dependency edge from an entry to a broad shared chunk |
| 267 | +- whether a large shared chunk exists only because of a fixed `name` |
| 268 | + |
| 269 | +## FAQ |
| 270 | + |
| 271 | +### Why do I still see duplicate modules? |
| 272 | + |
| 273 | +Common reasons: |
| 274 | + |
| 275 | +- the shared candidate is too small, so extracting it would not satisfy `minSize` |
| 276 | +- the candidate does not satisfy `minSizeReduction` |
| 277 | +- it does not satisfy `minChunks` |
| 278 | +- request-budget limits reject the split |
| 279 | +- `chunks` / `test` / `cacheGroups` do not actually select the same chunk combination |
| 280 | + |
| 281 | +If the duplicate module is tiny, do not assume this is a bug. Rspack may intentionally keep it in place because splitting it out would create a worse chunk. |
| 282 | + |
| 283 | +### Does splitChunks affect JS execution order? |
| 284 | + |
| 285 | +No. |
| 286 | + |
| 287 | +- `splitChunks` only changes chunk boundaries and dependency edges |
| 288 | +- JS loading and execution order are runtime concerns |
| 289 | +- if a JS ordering bug appears, investigate runtime/bootstrap, side effects, or app code first |
| 290 | + |
| 291 | +### Does splitChunks affect tree shaking? |
| 292 | + |
| 293 | +No. |
| 294 | + |
| 295 | +- tree shaking is controlled by module-graph analysis such as `sideEffects`, `usedExports`, and dead-code elimination |
| 296 | +- `splitChunks` runs later and only reorganizes already-selected modules into chunks |
| 297 | +- `splitChunks.usedExports` is only a grouping hint for runtime-specific chunk combinations; it is not tree shaking itself |
| 298 | + |
| 299 | +### Can splitChunks affect CSS order? |
| 300 | + |
| 301 | +Yes, potentially. |
| 302 | + |
| 303 | +- this caveat applies to CSS order, not JS execution order |
| 304 | +- extracted CSS flows such as `mini-css-extract-plugin` or `experiments.css` can observe changed final CSS order after splitChunks rewrites chunk groups |
| 305 | +- if CSS order is critical, be careful when splitting order-sensitive styles into separate chunks |
| 306 | + |
| 307 | +See [web-infra-dev discussion #12](https://github.com/orgs/web-infra-dev/discussions/12). |
| 308 | + |
| 309 | +## Quick conclusions to reuse |
| 310 | + |
| 311 | +- "Keep `chunks: \"all\"`, keep the default cache groups, and remove `name` unless you intentionally want forced sharing." |
| 312 | +- "`name` is not just a filename hint in Rspack splitChunks; it changes grouping behavior." |
| 313 | +- "`splitChunks` does not control JS execution order or tree shaking; it only changes chunk topology." |
| 314 | +- "`splitChunks` can affect CSS order in extracted-CSS scenarios, so treat CSS as a separate caveat." |
| 315 | +- "`maxSize` is the safer tool when the problem is one chunk being too large." |
0 commit comments