Skip to content

optimization.inlineExports keeps stale inlined constant values in consumers when reusing the filesystem cache #22019

Closed

Description

@YuyaYoshioka

Have you used AI?

Yes

Which part of webpack is affected?

cache (persistent/filesystem cache), optimization (tree-shaking, splitChunks, concatenation)

Bug Description

When optimization.inlineExports is enabled (the default in production mode since v5.108.0) together with cache.type: "filesystem", changing the value of an inlined constant export does not invalidate the code generation cache of the modules that consume it.

Consuming modules keep the value that was inlined during a previous build, so the emitted bundle silently differs from what a cold build produces for the same inputs.

This affects both:

  • values injected via DefinePlugin / EnvironmentPlugin (e.g. export const FLAG = process.env.FLAG)
  • plain source constants (e.g. export const NUM = 5 changed to 6)

Link to Minimal Reproduction and steps to reproduce

https://github.com/YuyaYoshioka/webpack-inline-exports-cache-repro

git clone https://github.com/YuyaYoshioka/webpack-inline-exports-cache-repro
cd webpack-inline-exports-cache-repro
npm ci

The whole project is three files:

src/env.js

export const FLAG = process.env.FLAG;
export const NUM = 5;

src/index.js

import { FLAG, NUM } from "./env";

if (FLAG === "on") {
  console.log("DEBUG_BLOCK_VISIBLE");
}

console.log("NUM_PLUS_ONE", NUM + 1);

webpack.config.js uses mode: "production", cache: { type: "filesystem" },
optimization: { minimize: false } and new webpack.EnvironmentPlugin({ FLAG: null }).
minimize is off so that the inlined literals stay visible in the output.

Case A — a DefinePlugin value changes

npm run clean
FLAG=on  npm run build && grep -o 'if ((.*=== "on")' dist/out.js
FLAG=off npm run build && grep -o 'if ((.*=== "on")' dist/out.js

Case B — a plain source constant changes

src/index.js is not modified in this case; only the provider changes.

npm run clean
FLAG=off npm run build && grep -o 'NUM_PLUS_ONE.*' dist/out.js

sed -i '' 's/NUM = 5/NUM = 6/' src/env.js   # GNU sed: sed -i 's/NUM = 5/NUM = 6/'
FLAG=off npm run build && grep -o 'NUM_PLUS_ONE.*' dist/out.js

Control — a cold build

npm run clean
FLAG=off npm run build && grep -o 'NUM_PLUS_ONE.*' dist/out.js

Expected Behavior

A build that reuses the filesystem cache should produce the same output as a cold build with the same inputs.

  • Case A: the second build emits if ((/* inlined export .FLAG */"off") === "on").
  • Case B: the second build emits (/* inlined export .NUM */6) + 1.

Deleting the cache directory and rebuilding does produce exactly this output, which confirms the inputs are correct.

Actual Behavior

The consuming module keeps the previously inlined literal.

  • Case A: the second build still emits if ((/* inlined export .FLAG */"on") === "on"), even though FLAG is now "off".
  • Case B: the second build still emits (/* inlined export .NUM */5) + 1.

Setting optimization.inlineExports: false produces the correct output in both cases, as does webpack v5.107.2 (where the option did not exist yet).

webpack inlineExports Case A (env value) Case B (source constant)
5.107.2 n/a (option absent) correct correct
5.108.0 default (true) stale stale
5.109.2 default (true) stale stale
5.109.2 false correct correct
5.110.3 default (true) stale stale
5.110.3 false correct correct

Environment

⬡ webpack info
────────────────────────────────────────────────────────────────────────
System and environment information.


⬡ System:
────────────────────────────────────────────────────────────────────────
  OS:      macOS 15.5
  CPU:     (12) arm64 Apple M3 Pro
  Memory:  442.55 MB / 36.00 GB

⬡ Binaries:
────────────────────────────────────────────────────────────────────────
  Node:  22.19.0 - /Users/yuya.yoshioka/.nodenv/versions/22.19.0/bin/node
  Yarn:  1.22.22 - /Users/yuya.yoshioka/.nodenv/versions/22.19.0/bin/yarn
  npm:   11.12.1 - /Users/yuya.yoshioka/.nodenv/versions/22.19.0/bin/npm

⬡ Browsers:
────────────────────────────────────────────────────────────────────────
  Chrome:  152.0.7977.83
  Safari:  18.5

⬡ Packages:
────────────────────────────────────────────────────────────────────────
  webpack:      5.110.3 → 5.110.3
  webpack-cli:  ^7.2.3 → 7.2.3
────────────────────────────────────────────────────────────────────────
ℹ Run 'webpack --help=verbose' to see all available commands and options.

Webpack documentation:  https://webpack.js.org/
CLI documentation:      https://webpack.js.org/api/cli/
Made with ♥ by the webpack team

Is this a regression?

Yes (please specify version below)

Last Working Version

v5.107.2

Additional Context

Additional observations

I have not traced this in webpack's source, so I cannot say where the invalidation is missed. The following are observations from running builds. The first two can be reproduced directly with the linked repository; the third needs a few extra exports added to src/env.js.

A change that crosses the "inlinable" size boundary is detected; a change that stays inside it is not.

Starting from export const NUM = 5; and building once, then changing the value and rebuilding with the same cache:

2nd build changes NUM to inlinable? result
6 (1 digit) yes → yes (/* inlined export .NUM */5) + 1 — stale
1234567 (7 digits) yes → no NUM + 1 — updated correctly

So the problem is not "the cache never notices this module". Something about the change is noticed when the value stops being inlinable, but a change from one inlinable value to another goes unnoticed.

Deleting the cache directory always produces the correct output, which is how I concluded the inputs are fine and only the cached build is wrong.

Which declarations are affected. Testing declaration shapes one by one matches the documented scope of the option ("Inline ESM exports that bind to small primitive constants (≤6-byte null/undefined/boolean/number/string)"): export default, computed expressions such as 1 + 2, and access through import * as ns are inlined, while let, objects and arrays are not. Booleans, null and undefined are inlined regardless of length.

I also noticed #20800, which looks similar in nature (a code generation cache not reflecting connection changes). I have not verified whether the two are actually related.

Why this matters

The failure is silent. Nothing errors, nothing is logged, and the build succeeds — the bundle is simply generated from stale code. The only ways to notice are to diff against a cold build or to inspect the emitted output directly.

Because inlineExports is on by default in production and cache.type: "filesystem" is widely used — including CI setups that persist the cache across builds — I suspect other projects may be affected without noticing. A plain npm run build run twice with a constant change in between is enough to trigger it.

Workaround

optimization: { inlineExports: false }

Note on AI usage

Per the AI policy: I used an AI assistant to help investigate this. All of the results above come from builds I ran myself, and I verified the reproduction, the version boundary, and the workaround before filing. I am happy to answer questions and to test patches.

Activity

  1. added theissue type on Sep 9, 2026
  2. added
    area: cachePersistent and memory caching, serialization (lib/cache, lib/serialization)
    area: configOptions, defaults, validation (lib/config, schemas/)
    area: optimizationTree-shaking, splitChunks, concatenation, ids (lib/optimize, lib/ids)
    regressionWorked in a previous release; pair with the Affected version field
    on Sep 9, 2026
  3. self-assigned this
    on Sep 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area: cachePersistent and memory caching, serialization (lib/cache, lib/serialization)area: configOptions, defaults, validation (lib/config, schemas/)area: optimizationTree-shaking, splitChunks, concatenation, ids (lib/optimize, lib/ids)regressionWorked in a previous release; pair with the Affected version field

Type

Fields

Priority

None yet

Effort

None yet

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions