Skip to content

[Feature]: More aggressive module concatenation for modules shared across async chunks #13093

@kimjh12

Description

@kimjh12

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 },
    },
  },
}

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions