#20536·webpack

ModuleConcatenationPlugin bails out for modules in available parent chunks ("not in the same chunk")

Author: kimjh12Created Feb 25, 2026Updated Sep 2, 2026

Bug report

When splitChunks extracts shared ESM modules into a separate async chunk (e.g. shared), ModuleConcatenationPlugin refuses to concatenate those modules into the async page chunks that import them. The bailout reason is "Module is not in the same chunk(s)".

The shared chunk is a parent chunk — it is guaranteed to be loaded before the async page chunk executes. So it should be safe to inline/concatenate those modules into the page scope, but webpack does not do this.

The root cause is unclear, probably related to module processing order which depends on module_graph_modules() iteration order. If NamedModulesPlugin vs HashedModuleIdsPlugin subtly affects module identity or iteration order, the greedy algorithm can amplify a small ordering difference into wildly different concatenation groups.

What is the current behavior?

With splitChunks extracting shared modules into a shared chunk, ModuleConcatenationPlugin emits:

ModuleConcatenation bailout: Cannot concat with ./src/shared/SharedComponent.js:
  Module is not in the same chunk(s) (expected in chunk(s) page-b, but module is in chunk(s) shared)

Each shared module stays as a separate __webpack_require__() call in the page chunk. This adds per-module wrapper overhead (factory function, Object.defineProperty exports, etc.) that hurts parse/compile time

In a real app with ~5,000 such bailouts across async chunks, this causes measurable runtime regression in webpack5 (vs webpack4).

What is the expected behavior?

If a module lives in a parent chunk that is guaranteed to load before the child async chunk, ModuleConcatenationPlugin should be allowed to concatenate it into the child chunk's scope. The module stays in the parent chunk too (so other consumers still work), but the child chunk gets the inlined version.

Steps to reproduce

Minimal repo: https://github.com/kimjh12/rspack-repro

Or inline — create these files:

webpack.config.mjs
import path from "path";
import { fileURLToPath } from "url";

const __dirname = path.dirname(fileURLToPath(import.meta.url));

export default {
  mode: "production",
  entry: { main: "./src/index.js" },
  output: {
    clean: true,
    path: path.resolve(__dirname, "dist"),
    filename: "[name].js",
    chunkFilename: "[name].js",
  },
  resolve: { extensions: [".js"] },
  module: {
    rules: [{ test: /\.js$/, resolve: { fullySpecified: false } }],
  },
  optimization: {
    concatenateModules: true,
    minimize: false,
    splitChunks: {
      chunks: "all",
      minSize: 0,
      cacheGroups: {
        shared: {
          test: /[\\/]shared[\\/]/,
          name: "shared",
          chunks: "async",
          minChunks: 2,
          enforce: true,
          priority: 10,
        },
      },
    },
  },
  stats: { optimizationBailout: true },
};
src/index.js
async function main() {
  const hash = location.hash;
  if (hash === "#b") {
    const { render } = await import(/* webpackChunkName: "page-b" */ "./pages/page-b/index.js");
    render();
  } else {
    const { render } = await import(/* webpackChunkName: "page-a" */ "./pages/page-a/index.js");
    render();
  }
}
main();
src/shared/SharedComponent.js
export const SharedComponent = (tag, props, children) => {
  const el = document.createElement(tag);
  if (props) Object.entries(props).forEach(([k, v]) => el.setAttribute(k, v));
  if (children) el.innerHTML = children;
  return el;
};

export const SharedWrapper = (child) => {
  const wrapper = document.createElement("div");
  wrapper.className = "shared-wrapper";
  wrapper.appendChild(child);
  return wrapper;
};
src/shared/SharedUtil.js
export const formatValue = (v) => String(v).toUpperCase();
export const formatLabel = (label, value) => `${label}: ${formatValue(value)}`;
src/pages/page-a/helpers.js
export const computeScore = (a, b) => a * 10 + b;
export const getTitle = () => "Page A Section";
src/pages/page-a/Section.js
import { SharedComponent, SharedWrapper } from "../../shared/SharedComponent";
import { formatValue } from "../../shared/SharedUtil";
import { computeScore, getTitle } from "./helpers";

export const renderSection = (a, b) => {
  const score = computeScore(a, b);
  const label = formatValue(getTitle());
  const content = SharedComponent("span", { class: "score" }, `${label}: ${score}`);
  return SharedWrapper(content);
};
src/pages/page-a/index.js
import { renderSection } from "./Section";
export const render = () => document.body.appendChild(renderSection(3, 7));
src/pages/page-b/index.js
import { SharedComponent, SharedWrapper } from "../../shared/SharedComponent";
import { formatLabel } from "../../shared/SharedUtil";

export const render = () => {
  const rank = Math.floor(42 / 10) + 1;
  const label = formatLabel("rank", String(rank));
  const content = SharedComponent("span", { class: "rank" }, label);
  document.body.appendChild(SharedWrapper(content));
};

Run:

npx webpack --stats verbose 2>&1 | grep "ModuleConcatenation bailout"

Output shows:

ModuleConcatenation bailout: Cannot concat with ./src/shared/SharedComponent.js
ModuleConcatenation bailout: Cannot concat with ./src/shared/SharedUtil.js

The page-a chunk concatenates its local modules (helpers.js, Section.js, index.js) fine, but cannot concatenate the shared modules from the parent shared chunk. The page-b chunk has zero concatenation because all its imports come from the shared chunk.

Other relevant information: webpack version: 5.102.1 Node.js version: v20.20.0 Operating System: Linux 5.15