-
-
Notifications
You must be signed in to change notification settings - Fork 767
Description
Summary
we traced rspack's concatenateModules (scope hoisting) being less aggressive than webpack's ModuleConcatenationPlugin, resulting in more module boundaries (function wrappers) that increase parse/compile time.
Using stats.optimizationBailout, we found bailout reasons across the build,
The dominant bailout reasons are "Module is not in the same chunk(s)" and "Referenced from different chunks". These are shared utility modules that appear across multiple async chunks (created by a dynamic import())
These modules live in the common chunk but are imported by modules in async chunks. Rspack refuses to concatenate them because they're "not in the same chunk" as the importing module. Webpack's ModuleConcatenationPlugin handled this case more aggressively — it would concatenate the shared module into one of its consuming chunks (or into the chunk that contained the most consumers).
Expected behavior
Rspack's concatenateModules should be able to concatenate ESM modules that are in a parent chunk (e.g., common) with modules in a child async chunk, similar to how webpack handles this case. The "not in the same chunk" bailout should not apply when the shared module is in an available parent chunk.
Reproduction
Setup: SharedComponent.tsx is in the common chunk (parent). PageSection.tsx and helpers.ts are in an async page chunk.
common chunk (loaded first, always available)
├── SharedComponent.tsx ← shared primitive, used across many async chunks
└── ...
page-MyPage chunk (async, loaded after common)
├── PageSection.tsx ← imports SharedComponent
├── helpers.ts ← pure utility, same async chunk
└── ...
The code:
// src/components/SharedComponent.tsx (in common chunk)
export const SharedComponent = (props) => <div {...props} />;
// src/pages/my-page/helpers.ts (in async chunk)
export const formatValue = (v) => String(v).toUpperCase();
// src/pages/my-page/PageSection.tsx (in async chunk)
import { SharedComponent } from "components/SharedComponent";
import { formatValue } from "./helpers";
export const PageSection = ({ value }) => (
<SharedComponent>{formatValue(value)}</SharedComponent>
);What webpack does: PageSection.tsx and helpers.ts are both in the same async chunk. SharedComponent is in the parent common chunk — guaranteed loaded before the async chunk executes. Webpack concatenates PageSection + helpers into a single scope:
var formatValue = (v) => String(v).toUpperCase();
var PageSection = ({ value }) => jsx(SharedComponent, { children: formatValue(value) });What rspack does: Bails out of concatenation entirely:
ModuleConcatenation bailout: Cannot concat with ./src/components/SharedComponent.tsx:
Module is not in the same chunk(s) (expected in chunk(s) page-MyPage,
but is in chunk(s) common)
Even though helpers.ts is in the same chunk as PageSection.tsx and could be concatenated, rspack refuses because another dependency (SharedComponent) is in a different chunk. The result is separate module wrappers:
123: (e, t, n) => {
n.d(t, { A: () => formatValue });
var formatValue = (v) => String(v).toUpperCase();
},
456: (e, t, n) => {
n.d(t, { A: () => PageSection });
var _shared = n(789); // cross-chunk import (fine at runtime)
var _helpers = n(123); // same-chunk import (could have been concatenated)
var PageSection = ({ value }) => jsx(_shared.A, { children: (0, _helpers.A)(value) });
},If rspack had concatenated modules 123 and 456 into a single scope, the code would instead be:
456: (e, t, n) => {
var formatValue = (v) => String(v).toUpperCase();
var _shared = n(789);
var PageSection = ({ value }) => jsx(_shared.A, { children: formatValue(value) });
}Environment
- Rspack version: 1.7.4
- Node.js version: 18.x
- OS: Linux
Configuration
optimization: {
concatenateModules: true, // production default
splitChunks: {
chunks: "all",
maxAsyncRequests: 5,
cacheGroups: {
defaultVendors: { test: /[\\/]node_modules[\\/]/, priority: -10 },
common: { minChunks: 5, priority: -20, reuseExistingChunk: true },
},
},
}