Skip to content

feat: support cross-module pure detection in inner graph#20907

Merged
alexander-akait merged 4 commits into
mainfrom
refactor-innergraph
May 25, 2026
Merged

feat: support cross-module pure detection in inner graph#20907
alexander-akait merged 4 commits into
mainfrom
refactor-innergraph

Conversation

@hai-x

@hai-x hai-x commented May 4, 2026

Copy link
Copy Markdown
Member

Summary

Refactors the InnerGraph tree-shaking utility so purity can be tracked across module boundaries, not just
within a single module.

What kind of change does this PR introduce?

TL;DR

A /*#__NO_SIDE_EFFECTS__*/ annotation in one module is now honored by call sites in other modules — not
just within the file it was declared in.
Example

// pure.js
/*#__NO_SIDE_EFFECTS__*/
export function pureFn(x) { return x; }

// index.js
import { pureFn } from "./pure";

const r = pureFn(1);   // result never read

Before this PR, pureFn shows up in usedExports because the /*#__NO_SIDE_EFFECTS__*/ hint stayed inside
pure.js and never reached the importer, so the call site pureFn(1) was treated as a usage with sideEffects.
After this PR, pureFn export can be tree-shaken.

How the optimization works:

  1. Producer records purity locally. While parsing pure.js, the parser sees the
    /*#__NO_SIDE_EFFECTS__*/ and writes the name into
    module.buildInfo.pureFunctions. At this point the purity is just a per-module fact.

  2. Purity is lifted onto the export descriptor. When HarmonyExportSpecifierDependency / HarmonyExportExpressionDependency build their ExportsSpec, they consult
    buildInfo.pureFunctions and emit { name: "pureFn", isPure: true }. This is the
    only channel that crosses the module boundary.

  3. FlagDependencyExportsPlugin propagates isPure to ExportInfo and the
    new isPure flag is copied onto the matching ExportInfo.

  4. Importer tags the local binding with a deferred pure predicate. When index.js is parsed, the inner graph
    creates a TopLevelSymbol for the imported pureFn and its pure field is a function for purpose of deferring instead of definite true. The deferral matters because the importing module may not yet be parsed.

  5. Per-compilation state. Inner-graph state is now scoped to the Compilation
    (getInnerGraph(compilation)) and keyed by Module, which is what lets step 4 reach into another module's
    ExportsInfo from inside the importer's graph.

  6. Inference resolves the call. When inferDependencyUsage walks the importer's graph, it reaches the
    pureFn(1) call, asks the symbol isPure(compilation, module), gets true, so propagate pure-symbol usage through the inner graph.

Did you add tests for your changes?

Yes

Does this PR introduce a breaking change?

No

If relevant, what needs to be documented once your changes are merged or what have you already documented?

No

Use of AI

Partial

@changeset-bot

changeset-bot Bot commented May 4, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 3f1d246

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
webpack Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@hai-x hai-x force-pushed the refactor-innergraph branch from 6e9739c to d6db533 Compare May 4, 2026 06:26
@github-actions

github-actions Bot commented May 4, 2026

Copy link
Copy Markdown
Contributor

This PR is packaged and the instant preview is available (c95b845).

Install it locally:

  • npm
npm i -D webpack@https://pkg.pr.new/webpack@c95b845
  • yarn
yarn add -D webpack@https://pkg.pr.new/webpack@c95b845
  • pnpm
pnpm add -D webpack@https://pkg.pr.new/webpack@c95b845

@codecov

codecov Bot commented May 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.48603% with 9 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.62%. Comparing base (75f60f6) to head (3f1d246).

Files with missing lines Patch % Lines
lib/optimize/InnerGraph.js 98.21% 3 Missing ⚠️
.../dependencies/HarmonyExportExpressionDependency.js 81.81% 2 Missing ⚠️
lib/optimize/InnerGraphPlugin.js 98.30% 2 Missing ⚠️
lib/optimize/SideEffectsFlagPlugin.js 91.66% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #20907      +/-   ##
==========================================
+ Coverage   91.59%   91.62%   +0.03%     
==========================================
  Files         573      573              
  Lines       59541    59664     +123     
  Branches    16077    16119      +42     
==========================================
+ Hits        54534    54668     +134     
+ Misses       5007     4996      -11     
Flag Coverage Δ
integration 89.55% <97.48%> (+0.04%) ⬆️
test262 45.40% <82.12%> (+0.09%) ⬆️
unit 37.87% <18.15%> (-0.09%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@codspeed-hq

codspeed-hq Bot commented May 4, 2026

Copy link
Copy Markdown

Merging this PR will degrade performance by 33.8%

⚡ 5 improved benchmarks
❌ 5 regressed benchmarks
✅ 134 untouched benchmarks
⏩ 72 skipped benchmarks1

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Memory benchmark "concatenate-modules", scenario '{"name":"mode-development","mode":"development"}' 1,113.6 KB 790.1 KB +40.95%
Memory benchmark "many-chunks-commonjs", scenario '{"name":"mode-development","mode":"development"}' 1,066.7 KB 826 KB +29.14%
Memory benchmark "context-esm", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 145.5 KB 660.6 KB -77.97%
Memory benchmark "concatenate-modules", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 485.8 KB 135 KB ×3.6
Memory benchmark "future-defaults", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 286.1 KB 363 KB -21.18%
Memory benchmark "cache-filesystem", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 178.8 KB 1,066.1 KB -83.23%
Memory benchmark "wasm-modules-async", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 1,553.6 KB 396.7 KB ×3.9
Memory benchmark "many-modules-esm", scenario '{"name":"mode-development","mode":"development"}' 1.3 MB 1.1 MB +24.14%
Memory benchmark "asset-modules-source", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 167 KB 3,721.5 KB -95.51%
Memory benchmark "many-chunks-commonjs", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 175.3 KB 451.6 KB -61.19%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing refactor-innergraph (3f1d246) with main (75f60f6)

Open in CodSpeed

Footnotes

  1. 72 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@hai-x hai-x force-pushed the refactor-innergraph branch 4 times, most recently from 9238fbc to 8c90e76 Compare May 6, 2026 18:38
@hai-x hai-x changed the title refactor: inner graph per compilation refactor: crossing module pure detection in inner graph May 11, 2026
@hai-x hai-x force-pushed the refactor-innergraph branch 3 times, most recently from e7c4f69 to bb09288 Compare May 16, 2026 19:12
Comment thread test/configCases/inner-graph/cross-module-pure-normal/index.js Fixed
Comment thread test/configCases/inner-graph/cross-module-pure-normal/index.js Fixed
Comment thread test/configCases/inner-graph/cross-module-pure-normal/index.js Fixed
@hai-x hai-x changed the title refactor: crossing module pure detection in inner graph feat: support cross-module pure detection in inner graph May 16, 2026
@hai-x hai-x marked this pull request as ready for review May 16, 2026 19:39
@hai-x hai-x force-pushed the refactor-innergraph branch 4 times, most recently from 5e7593a to 4f89ebf Compare May 18, 2026 14:55
commentsStartPos = /** @type {Range} */ (arg.range)[1];
return pureFlag;
});
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why we remove it from here? We use isPure in other places, so it can brings some regressions in optimizations

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. We ran into this problem before and tried moving it here as a workaround, but it's now properly fixed via pureConditionByCallExpr, so let's revert it back. Thanks for raising it.

@alexander-akait

Copy link
Copy Markdown
Member

@hai-x Looks good, only one question

Comment thread test/configCases/inner-graph/cross-module-pure-normal/index.js Fixed
Comment thread test/configCases/inner-graph/cross-module-pure-normal/index.js Fixed
Comment thread test/configCases/inner-graph/cross-module-pure-normal/index.js Fixed
Comment thread test/configCases/inner-graph/cross-module-pure-normal/index.js Fixed
@alexander-akait

Copy link
Copy Markdown
Member

@hai-x can you take a look at copilot review (should we add a test case for such usage), also can you rebase, thanks, I think it is time to prepare a new minor release

@hai-x hai-x force-pushed the refactor-innergraph branch from 85923e0 to bd992e9 Compare May 21, 2026 18:27
@hai-x hai-x force-pushed the refactor-innergraph branch from bd992e9 to bae60c5 Compare May 21, 2026 18:31
usedExportsOfPureSource
} from "./pure-source";

const pure1 = pureExport1(1);
} from "./pure-source";

const pure1 = pureExport1(1);
const pure2 = pureExport2(1);

const pure1 = pureExport1(1);
const pure2 = pureExport2(1);
const pure3 = pureExport3(1);
@hai-x

hai-x commented May 21, 2026

Copy link
Copy Markdown
Member Author

@alexander-akait It raise some issues about unused variable.

But for now we should keep the variable declarations as-is, because the inner graph currently collects top-level function, class, and variable declarations and doesn't detect top-level ExpressionStatements. It means that we will think pureExport1(1); as impure for now.

But I think we can support bare expression statements in a follow-up PR.

@alexander-akait

alexander-akait commented May 21, 2026

Copy link
Copy Markdown
Member

Got it, let's improve ExpressionStatements (and maybe more) in future too, I see many rooms to improve this more, but this let's merge as is

@hai-x hai-x force-pushed the refactor-innergraph branch from 827a1f1 to 6bfb5ca Compare May 23, 2026 08:43
Comment thread test/configCases/inner-graph/cross-module-pure-normal/index.js Fixed
Comment thread test/configCases/inner-graph/cross-module-pure-normal/index.js Fixed
Comment thread test/configCases/inner-graph/cross-module-pure-normal/index.js Fixed
Comment thread test/configCases/inner-graph/cross-module-pure-normal/index.js Fixed
@hai-x hai-x force-pushed the refactor-innergraph branch from 6bfb5ca to 3f1d246 Compare May 23, 2026 08:46
@github-actions

Copy link
Copy Markdown
Contributor

Types Coverage

Coverage after merging refactor-innergraph into main will be
98.97%
Coverage Report
FileStmtsBranchesFuncsLinesUncovered Lines
bin
   webpack.js98.77%100%100%98.77%91
examples
   build-common.js100%100%100%100%
   buildAll.js100%100%100%100%
   examples.js100%100%100%100%
   template-common.js98.21%100%100%98.21%72
examples/custom-javascript-parser
   test.filter.js100%100%100%100%
examples/custom-javascript-parser/internals
   acorn-parse.js100%100%100%100%
   meriyah-parse.js100%100%100%100%
   oxc-parse.js91.30%100%100%91.30%140, 142–143, 145, 147, 153–154, 161, 168, 90
examples/markdown
   webpack.config.mjs100%100%100%100%
examples/typescript
   test.filter.js100%100%100%100%
examples/typescript-non-erasable
   test.filter.js50%100%100%50%5
examples/virtual-modules
   test.filter.js100%100%100%100%
examples/wasm-bindgen-esm
   test.filter.js100%100%100%100%
examples/wasm-complex
   test.filter.js100%100%100%100%
examples/wasm-simple
   test.filter.js100%100%100%100%
examples/wasm-simple-source-phase
   test.filter.js100%100%100%100%
lib
   APIPlugin.js100%100%100%100%
   AsyncDependenciesBlock.js100%100%100%100%
   AutomaticPrefetchPlugin.js100%100%100%100%
   BannerPlugin.js100%100%100%100%
   Cache.js98.21%100%100%98.21%101
   CacheFacade.js100%100%100%100%
   Chunk.js99.72%100%100%99.72%39
   ChunkGraph.js100%100%100%100%
   ChunkGroup.js100%100%100%100%
   ChunkTemplate.js100%100%100%100%
   CleanPlugin.js99.15%100%100%99.15%206, 226
   CodeGenerationResults.js100%100%100%100%
   CompatibilityPlugin.js100%100%100%100%
   Compilation.js98.45%100%100%98.45%1572, 1868, 1875, 1883, 1905, 2801, 3226, 3888, 3917, 3970–3971, 3975, 3980, 3996–3997, 4011–4012, 4017–4018, 4495, 4521, 511, 516, 5229, 5261, 5278, 5294, 5310, 5325, 5350–5351, 5353, 5681, 5686, 5692, 5695, 5707, 5709, 5713, 5729, 5744, 5776, 5830, 5854, 5968, 730–731
   Compiler.js99.55%100%100%99.55%1116–1117, 1125
   ConcatenationScope.js98.59%100%100%98.59%189
   ConditionalInitFragment.js100%100%100%100%
   ConstPlugin.js100%100%100%100%
   ContextExclusionPlugin.js100%100%100%100%
   ContextModule.js100%100%100%100%
   ContextModuleFactory.js97.40%100%100%97.40%258, 395, 418, 420, 424, 433–434
   ContextReplacementPlugin.js100%100%100%100%
   DefinePlugin.js98.92%100%100%98.92%158–159, 175, 194, 268
   DependenciesBlock.js100%100%100%100%
   Dependency.js98.20%100%100%98.20%381, 427
   DependencyTemplate.js100%100%100%100%
   DependencyTemplates.js100%100%100%100%
   DotenvPlugin.js98.41%100%100%98.41%378, 391–392
   DynamicEntryPlugin.js100%100%100%100%
   EntryOptionPlugin.js100%100%100%100%
   EntryPlugin.js100%100%100%100%
   Entrypoint.js100%100%100%100%
   EnvironmentPlugin.js97.14%100%100%97.14%49
   ErrorHelpers.js100%100%100%100%
   EvalDevToolModulePlugin.js100%100%100%100%
   EvalSourceMapDevToolPlugin.js100%100%100%100%
   ExportsInfo.js100%100%100%100%
   ExportsInfoApiPlugin.js100%100%100%100%
   ExternalModule.js98.97%100%100%98.97%425–429, 577
   ExternalModuleFactoryPlugin.js100%100%100%100%
   ExternalsPlugin.js100%100%100%100%
   FileSystemInfo.js99.50%100%100%99.50%182, 2252–2253, 2256, 2267, 2278, 2289, 278, 3694, 3709, 3733
   FlagAllModulesAsUsedPlugin.js100%100%100%100%
   FlagDependencyExportsPlugin.js98.78%100%100%98.78%409, 411, 415
   FlagDependencyUsagePlugin.js100%100%100%100%
   FlagEntryExportAsUsedPlugin.js100%100%100%100%
   Generator.js100%100%100%100%
   HotModuleReplacementPlugin.js100%100%100%100%
   HotUpdateChunk.js100%100%100%100%
   IgnorePlugin.js100%100%100%100%
   IgnoreWarningsPlugin.js100%100%100%100%
   InitFragment.js100%100%100%100%
   JavascriptMetaInfoPlugin.js100%100%100%100%
   LibraryTemplatePlugin.js100%100%100%100%
   LoaderOptionsPlugin.js100%100%100%100%
   LoaderTargetPlugin.js100%100%100%100%
   MainTemplate.js100%100%100%100%
   ManifestPlugin.js100%100%100%100%
   Module.js98.50%100%100%98.50%1306, 1311, 1372, 1386, 1448, 1457
   ModuleFactory.js100%100%100%100%
   ModuleFilenameHelpers.js98.85%100%100%98.85%106, 108
   ModuleGraph.js99.73%100%100%99.73%1004
   ModuleGraphConnection.js100%100%100%100%
   ModuleInfoHeaderPlugin.js100%100%100%100%
   ModuleNotFoundError.js100%100%100%100%
   ModuleProfile.js100%100%100%100%
   ModuleSourceTypeConstants.js100%100%100%100%
   ModuleTemplate.js100%100%100%100%
   ModuleTypeConstants.js100%100%100%100%
   MultiCompiler.js99.69%100%100%99.69%645
   MultiStats.js100%100%100%100%
   MultiWatching.js100%100%100%100%
   NoEmitOnErrorsPlugin.js100%100%100%100%
   NodeStuffPlugin.js100%100%100%100%
   NormalModule.js98.11%100%100%98.11%1208, 1211, 1228, 1245, 1492, 1526, 1542, 1629, 2252, 2257–2267, 569
   NormalModuleFactory.js99.47%100%100%99.47%1083, 1392, 486, 498
   NormalModuleReplacementPlugin.js100%100%100%100%
   NullFactory.js100%100%100%100%
   OptimizationStages.js100%100%100%100%
   OptionsApply.js100%100%100%100%
   Parser.js100%100%100%100%
   PlatformPlugin.js100%100%100%100%
   PrefetchPlugin.js100%100%100%100%
   ProgressPlugin.js98.85%100%100%98.85%519–520, 525, 527, 591
   ProvidePlugin.js100%100%100%100%
   RawModule.js100%100%100%100%
   RecordIdsPlugin.js100%100%100%100%
   RequestShortener.js100%100%100%100%
   ResolverFactory.js100%100%100%100%
   RuntimeGlobals.js100%100%100%100%
   RuntimeModule.js100%100%100%100%
   RuntimePlugin.js100%100%100%100%
   RuntimeTemplate.js100%100%100%100%
   SelfModuleFactory.js100%100%100%100%
   SingleEntryPlugin.js100%100%100%100%
   SourceMapDevToolModuleOptionsPlugin.js100%100%100%100%
   SourceMapDevToolPlugin.js98.62%100%100%98.62%220, 224, 226, 419, 430, 891
   Stats.js100%100%100%100%
   Template.js100%100%100%100%
   TemplatedPathPlugin.js98.86%100%100%98.86%136–137
   UseStrictPlugin.js100%100%100%100%
   WarnCaseSensitiveModulesPlugin.js100%100%100%100%
   WarnDeprecatedOptionPlugin.js100%100%100%100%
   WarnNoModeSetPlugin.js100%100%100%100%
   WatchIgnorePlugin.js100%100%100%100%
   Watching.js100%100%100%100%
   WebpackError.js100%100%100%100%
   WebpackIsIncludedPlugin.js100%100%100%100%
   WebpackOptionsApply.js100%100%100%100%
   WebpackOptionsDefaulter.js100%100%100%100%
   buildChunkGraph.js99.87%100%100%99.87%325
   cli.js98.46%100%100%98.46%10, 119, 471, 503, 545, 815
   index.js99.72%100%100%99.72%165
   validateSchema.js94.67%100%100%94.67%100, 87, 89, 98
   webpack.js96.33%100%100%96.33%10, 198, 220, 222
lib/asset
   AssetBytesGenerator.js100%100%100%100%
   AssetBytesParser.js100%100%100%100%
   AssetGenerator.js100%100%100%100%
   AssetModulesPlugin.js97.32%100%100%97.32%283, 307, 310, 36, 362, 41
   AssetParser.js100%100%100%100%
   AssetSourceGenerator.js100%100%100%100%
   AssetSourceParser.js100%100%100%100%
   RawDataUrlModule.js100%100%100%100%
lib/async-modules
   AsyncModuleHelpers.js100%100%100%100%
   AwaitDependenciesInitFragment.js100%100%100%100%
   InferAsyncModulesPlugin.js100%100%100%100%
lib/cache
   AddBuildDependenciesPlugin.js100%100%100%100%
   AddManagedPathsPlugin.js100%100%100%100%
   IdleFileCachePlugin.js97.92%100%100%97.92%71, 83, 91
   MemoryCachePlugin.js95.83%100%100%95.83%33
   MemoryWithGcCachePlugin.js93.15%100%100%93.15%106, 113–114, 122, 89
   PackFileCacheStrategy.js96.40%100%100%96.40%1250, 1350, 1354, 1416, 628, 647, 657–659, 661, 677–678, 683, 686, 688, 693, 698, 722, 728, 762, 768, 774, 779, 790, 799, 804–805, 807, 824, 830–831, 833
   ResolverCachePlugin.js100%100%100%100%
   getLazyHashedEtag.js100%100%100%100%
   mergeEtags.js100%100%100%100%
lib/config
   browserslistTargetHandler.js100%100%100%100%
   defaults.js99.29%100%100%99.29%1411–1413, 1421, 271, 274, 279, 283
   normalization.js99%100%100%99%191–192, 258, 273
   target.js100%100%100%100%
lib/container
   ContainerEntryDependency.js100%100%100%100%
   ContainerEntryModule.js100%100%100%100%
   ContainerEntryModuleFactory.js100%100%100%100%
   ContainerExposedDependency.js100%100%100%100%
   ContainerPlugin.js100%100%100%100%
   ContainerReferencePlugin.js100%100%100%100%
   FallbackDependency.js100%100%100%100%
   

@@ -0,0 +1,16 @@
import * as ns from "./pure-source";

const pure1 = ns.pureExport1(1);
import * as ns from "./pure-source";

const pure1 = ns.pureExport1(1);
const pure2 = ns.pureExport2(1);

const pure1 = ns.pureExport1(1);
const pure2 = ns.pureExport2(1);
const pure3 = ns.pureExport3(1);
const pure1 = ns.pureExport1(1);
const pure2 = ns.pureExport2(1);
const pure3 = ns.pureExport3(1);
const pure4 = ns.pureExport4(1);
const pure2 = ns.pureExport2(1);
const pure3 = ns.pureExport3(1);
const pure4 = ns.pureExport4(1);
const pure5 = ns.pureExport5(1);
const pure3 = ns.pureExport3(1);
const pure4 = ns.pureExport4(1);
const pure5 = ns.pureExport5(1);
const pureDefault = ns.default(1);
const pure5 = ns.pureExport5(1);
const pureDefault = ns.default(1);

const impure = ns.impureExport(2);
@alexander-akait alexander-akait merged commit c95b845 into main May 25, 2026
64 of 66 checks passed
@alexander-akait alexander-akait deleted the refactor-innergraph branch May 25, 2026 15:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants