rspack-split-chunks

$npx mdskill add rstackjs/agent-skills/rspack-split-chunks

Diagnose and optimize Rspack's `optimization.splitChunks` for efficient production asset bundling.

  • Addresses module duplication, large shared chunks, and caching issues in web builds.
  • Interacts with Rspack's build configuration object, specifically `optimization.splitChunks`.
  • Analyzes user requirements against best practices for chunking strategies and cache groups.
  • Outputs structured recommendations detailing configuration adjustments for improved performance.
SKILL.md
.github/skills/rspack-split-chunksView on GitHub ↗
---
name: rspack-split-chunks
description: >-
  Diagnose and optimize Rspack `optimization.splitChunks` configuration. Use
  this when a user wants better production chunking, safer `chunks: "all"`
  defaults, fewer duplicated modules, better long-term caching, `cacheGroups`
  design help, `maxSize` tuning, or debugging over-fetch caused by `name` and
  forced chunk merging.
---

# Rspack SplitChunks Optimization

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.

## Default stance

- Distinguish repo defaults from recommended production baselines.
- Rspack's built-in default is `chunks: "async"`, but for most production web apps the best starting point is:

```js
optimization: {
  splitChunks: {
    chunks: "all",
  },
}
```

- Keep the default cache groups unless there is a concrete reason to replace them.
- Treat `name` as a graph-shaping option, not a cosmetic naming option.
- 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.

Read [`references/repo-behavior.md`](references/repo-behavior.md) when you need the source-backed rationale.

## What To Optimize For

First identify which problem the user actually has:

- duplicated modules across entry or async boundaries
- a route fetching a large shared chunk with mostly unused modules
- too many tiny chunks
- a vendor/common chunk that changes too often and hurts caching
- an oversized async or initial chunk that should be subdivided
- confusion about whether `splitChunks` affects runtime execution order

Do not optimize all of these at once. Pick the primary goal and keep the rest as constraints.

## Workflow

### 1. Start from the safest production baseline

Unless the user already has a measured problem that requires custom grouping, prefer:

```js
optimization: {
  splitChunks: {
    chunks: "all",
  },
}
```

Why:

- it lets splitChunks dedupe modules across both initial and async chunks
- it still only loads chunks reachable from the current entry/runtime
- it usually avoids loading unnecessary modules better than hand-written global vendor buckets

If the existing config disables `default` or `defaultVendors`, assume that is suspicious until proven necessary.

### 2. Audit the config for high-risk knobs

Check these first:

- fixed `name`
- `cacheGroups.*.name`
- `enforce: true`
- disabled `default` / `defaultVendors`
- broad `test: /node_modules/` rules combined with a single global `name`
- `usedExports: false`
- very small `minSize`
- `maxSize` combined with manual global names

### 3. Interpret `name` correctly

Use this rule:

- No `name`: splitChunks can keep different chunk combinations separate.
- Same `name`: matching modules are merged into the same named split chunk candidate.

That means a fixed `name: "vendors"` or `name: "common"` is often the real reason a page starts fetching modules from unrelated dependency chains.

Prefer these alternatives before adding `name`:

- keep `name` unset
- use `idHint` if the goal is filename identity, not grouping identity
- narrow the `test` so the cache group is smaller
- split one broad cache group into several focused cache groups
- rely on `maxSize` to subdivide a big chunk instead of forcing a global name

Use a fixed `name` only when the user explicitly wants one shared asset across multiple entries/routes and accepts the extra coupling.

### 4. Preserve the built-in cache groups by default

Rspack's built-in production-oriented behavior depends heavily on these two groups:

- `default`: extracts modules shared by at least 2 chunks and reuses existing chunks
- `defaultVendors`: extracts `node_modules` modules and reuses existing chunks

These defaults are usually the best balance between dedupe and "only fetch what this page needs".

If you customize `cacheGroups`, do not casually replace these with one manually named vendor bucket.

### 5. Use `chunks: "all"` without fear of breaking execution order

When a module group is split out, Rspack connects the new chunk back to the original chunk groups. That preserves JavaScript loading semantics.

So:

- `splitChunks` changes chunk topology
- the runtime still guarantees dependency loading/execution order
- if execution order appears broken, look for other causes first
- this statement is about JavaScript, not CSS order

### 6. Use `maxSize` as a refinement tool

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".

Important behavior:

- `maxSize` runs after a chunk already exists
- the split is deterministic
- modules are grouped by path-derived keys and split near low-similarity boundaries
- similar file paths tend to stay together

This is usually safer than forcing one giant named vendor chunk, because it keeps chunk graph semantics while subdividing hot spots.

### 7. Use `usedExports` deliberately

If the user has multiple runtimes/entries and wants leaner shared chunks per runtime, prefer keeping `usedExports` enabled.

If they set `usedExports: false`, expect broader sharing and potentially larger common chunks.

This is still not tree shaking. It only changes how splitChunks groups modules across runtimes.

### 8. Treat `enforce: true` as an escape hatch

`enforce: true` bypasses several normal guardrails. Use it only when the user intentionally wants a split regardless of `minSize`, `minChunks`, and request limits.

If a config looks aggressive and hard to explain, check `enforce` before changing anything else.

## Recommendations By Goal

### Better default production chunking

Recommend:

```js
optimization: {
  splitChunks: {
    chunks: "all",
  },
}
```

Avoid:

- disabling `default`
- disabling `defaultVendors`
- adding `name` before measuring a real problem

### Avoid fetching non-essential modules

Recommend:

- remove fixed `name`
- keep cache groups narrow
- keep `chunks: "all"` if dedupe across initial chunks is still desired
- inspect which routes now depend on a shared chunk after each change

Avoid:

```js
cacheGroups: {
  vendors: {
    test: /[\\/]node_modules[\\/]/,
    chunks: "all",
    name: "vendors",
    enforce: true
  }
}
```

That pattern often creates one over-shared chunk that many pages must fetch.

### Improve caching without over-merging

Recommend:

- keep `name` unset
- use `idHint`
- keep `chunkIds: "deterministic"` or other stable id strategies elsewhere in the config
- split broad groups into smaller focused groups only when the package boundaries are stable and important

Use a fixed `name` only if the user explicitly prefers cache reuse over route isolation.

### Split a large shared chunk

Recommend:

```js
optimization: {
  splitChunks: {
    chunks: "all",
    maxSize: 200000,
  },
}
```

Then tune:

- `maxAsyncSize` when async chunks are the pain point
- `maxInitialSize` when first-load pressure matters more
- `hidePathInfo` if generated part names should not leak path structure

### Keep an intentionally shared chunk

Recommend a named chunk only when the user says something like:

- "all pages should share one React vendor asset"
- "I want one framework chunk for cache reuse across routes"

Even then, call out the tradeoff explicitly:

- better cache hit rate
- more coupling between routes
- a page may fetch modules it does not execute immediately

## Review Checklist

When reviewing a user's config, explicitly answer:

1. Is the goal dedupe, cache stability, request count, or route isolation?
2. Is `chunks: "all"` a better baseline than the current config?
3. Did `name` accidentally turn multiple candidates into one forced shared chunk?
4. Were `default` or `defaultVendors` disabled without a strong reason?
5. Would `idHint` satisfy the naming goal without changing grouping?
6. Is `maxSize` a better fit than a broad manual vendor/common bucket?
7. Does the result still keep each page fetching only reachable chunks?

## Minimal stats setup

When the task includes diagnosis, ask for or generate stats that expose chunk relations:

```js
stats: {
  chunks: true,
  chunkRelations: true,
  chunkOrigins: true,
  entrypoints: true,
  modules: false
}
```

Then compare:

- which entrypoints reference which shared chunks
- whether a change added a new dependency edge from an entry to a broad shared chunk
- whether a large shared chunk exists only because of a fixed `name`

## FAQ

### Why do I still see duplicate modules?

Common reasons:

- the shared candidate is too small, so extracting it would not satisfy `minSize`
- the candidate does not satisfy `minSizeReduction`
- it does not satisfy `minChunks`
- request-budget limits reject the split
- `chunks` / `test` / `cacheGroups` do not actually select the same chunk combination

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.

### Does splitChunks affect JS execution order?

No.

- `splitChunks` only changes chunk boundaries and dependency edges
- JS loading and execution order are runtime concerns
- if a JS ordering bug appears, investigate runtime/bootstrap, side effects, or app code first

### Does splitChunks affect tree shaking?

No.

- tree shaking is controlled by module-graph analysis such as `sideEffects`, `usedExports`, and dead-code elimination
- `splitChunks` runs later and only reorganizes already-selected modules into chunks
- `splitChunks.usedExports` is only a grouping hint for runtime-specific chunk combinations; it is not tree shaking itself

### Can splitChunks affect CSS order?

Yes, potentially.

- this caveat applies to CSS order, not JS execution order
- extracted CSS flows such as `mini-css-extract-plugin` or `experiments.css` can observe changed final CSS order after splitChunks rewrites chunk groups
- if CSS order is critical, be careful when splitting order-sensitive styles into separate chunks

See [web-infra-dev discussion #12](https://github.com/orgs/web-infra-dev/discussions/12).

## Quick conclusions to reuse

- "Keep `chunks: \"all\"`, keep the default cache groups, and remove `name` unless you intentionally want forced sharing."
- "`name` is not just a filename hint in Rspack splitChunks; it changes grouping behavior."
- "`splitChunks` does not control JS execution order or tree shaking; it only changes chunk topology."
- "`splitChunks` can affect CSS order in extracted-CSS scenarios, so treat CSS as a separate caveat."
- "`maxSize` is the safer tool when the problem is one chunk being too large."
More from rstackjs/agent-skills