Skip to content

feat: optimize export bindings with value descriptors for static exports#21021

Merged
alexander-akait merged 1 commit into
mainfrom
feat/static-export-bindings
May 28, 2026
Merged

feat: optimize export bindings with value descriptors for static exports#21021
alexander-akait merged 1 commit into
mainfrom
feat/static-export-bindings

Conversation

@xiaoxiaojx

@xiaoxiaojx xiaoxiaojx commented May 22, 2026

Copy link
Copy Markdown
Member

Summary

Resolves #18494

Currently webpack wraps every ES module export in a getter via __webpack_require__.d():

__webpack_require__.d(__webpack_exports__, {
    value: () => value  // getter — unnecessary for const
});

The getter indirection preserves live binding semantics, but const exports are never reassigned and don't need it. This PR uses Object.defineProperty with value descriptors instead of getters for const exports, reducing runtime overhead.

Implementation:

  1. Detects const bindingsConstValueParserPlugin (extended from inlineExports) records all top-level const declaration names (including destructuring patterns) in buildInfo.constBindings
  2. Detects circular modulesCircularModulesPlugin + CycleGraph builds the synchronous dependency graph and runs iterative Tarjan's SCC, marking circular modules via buildInfo.isCircular
  3. Emits value descriptorsExportBindingInitFragment generates a flat-array __webpack_require__.d() call with 0 sentinel for value bindings: ["key", 0, value]

Optimization rules:

  • Only const declarations are eligible (function/class names can be reassigned in sloppy mode)
  • Circular modules are excluded (value may be undefined at import time)
  • Re-exports always use getters (cross-module bindings may be mutable, and SideEffectsFlagPlugin can rewire connections)
  • Gated behind optimization.inlineExports (defaults to true in production)

Inspired by web-infra-dev/rspack#14045

What kind of change does this PR introduce?

feat

Did you add tests for your changes?

Yes — test/configCases/optimization/static-export-bindings/ covers const exports (literal, renamed, destructured, array), function exports (getter retained), mutable let exports (getter retained), circular modules, and re-exports.

Does this PR introduce a breaking change?

No. The optimization is gated behind the existing optimization.inlineExports option. The runtime __webpack_require__.d() now supports both array and object formats, with the object format retained for backward compatibility.

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

n/a

Use of AI

Claude Code was used to draft the implementation under human review. All architectural decisions were made through interactive discussion and iterative review.

@changeset-bot

changeset-bot Bot commented May 22, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: ed29f43

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

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

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

@xiaoxiaojx xiaoxiaojx marked this pull request as draft May 22, 2026 13:24
@github-actions

github-actions Bot commented May 22, 2026

Copy link
Copy Markdown
Contributor

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

Install it locally:

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

@codecov

codecov Bot commented May 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.39548% with 17 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.72%. Comparing base (1a17812) to head (ed29f43).

Files with missing lines Patch % Lines
lib/dependencies/ExportBindingInitFragment.js 69.64% 17 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #21021      +/-   ##
==========================================
- Coverage   91.73%   91.72%   -0.01%     
==========================================
  Files         577      580       +3     
  Lines       60409    60582     +173     
  Branches    16352    16396      +44     
==========================================
+ Hits        55415    55571     +156     
- Misses       4994     5011      +17     
Flag Coverage Δ
integration 89.61% <90.39%> (+<0.01%) ⬆️
test262 45.78% <88.70%> (+0.15%) ⬆️
unit 38.27% <47.45%> (+0.03%) ⬆️

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.

@xiaoxiaojx xiaoxiaojx force-pushed the feat/static-export-bindings branch from d7fa1f7 to afd7f8a Compare May 22, 2026 13:51
@linux-foundation-easycla

linux-foundation-easycla Bot commented May 22, 2026

Copy link
Copy Markdown

CLA Signed
The committers listed above are authorized under a signed CLA.

  • ✅ login: xiaoxiaojx / name: xiaoxiaojx (4bc8cc648a07d769a11250f667cd5b66251e661b)

@codspeed-hq

codspeed-hq Bot commented May 22, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

⚡ 5 improved benchmarks
❌ 7 regressed benchmarks
✅ 132 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 "many-chunks-esm", scenario '{"name":"mode-production","mode":"production"}' 9.1 MB 7.4 MB +23.11%
Memory benchmark "react", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 133.1 KB 395.1 KB -66.32%
Memory benchmark "many-chunks-commonjs", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 247.9 KB 176.7 KB +40.28%
Memory benchmark "many-modules-commonjs", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 197.8 KB 278.1 KB -28.86%
Memory benchmark "context-esm", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 979.1 KB 251 KB ×3.9
Memory benchmark "future-defaults", scenario '{"name":"mode-production","mode":"production"}' 7.7 MB 10.1 MB -23.19%
Memory benchmark "cache-filesystem", scenario '{"name":"mode-production","mode":"production"}' 3.8 MB 4.8 MB -20.08%
Memory benchmark "future-defaults", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 328.5 KB 524.2 KB -37.33%
Memory benchmark "context-esm", scenario '{"name":"mode-production","mode":"production"}' 10.3 MB 8.1 MB +27.16%
Memory benchmark "many-modules-esm", scenario '{"name":"mode-production","mode":"production"}' 7.3 MB 9.3 MB -21.24%
Memory benchmark "cache-filesystem", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 845.7 KB 693.2 KB +22.01%
Memory benchmark "asset-modules-bytes", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 247.3 KB 322.8 KB -23.38%

Tip

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


Comparing feat/static-export-bindings (ed29f43) with main (1a17812)

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.

@xiaoxiaojx xiaoxiaojx force-pushed the feat/static-export-bindings branch 9 times, most recently from c1631d4 to 62a4692 Compare May 22, 2026 17:58
@alexander-akait

Copy link
Copy Markdown
Member

@xiaoxiaojx Looks like we need to make rebase

@xiaoxiaojx

Copy link
Copy Markdown
Member Author

@alexander-akait Yeah, let’s merge #20973 first. Looks like both PRs need the ConstValuePlugin logic, so we can reuse it after the merge.

@xiaoxiaojx xiaoxiaojx force-pushed the feat/static-export-bindings branch 2 times, most recently from 629658a to 9bf7a90 Compare May 27, 2026 14:33
@alexander-akait

Copy link
Copy Markdown
Member

@xiaoxiaojx merged, feel free to rebase

@xiaoxiaojx xiaoxiaojx force-pushed the feat/static-export-bindings branch 4 times, most recently from 670022a to 44012f0 Compare May 28, 2026 05:10
@xiaoxiaojx xiaoxiaojx marked this pull request as ready for review May 28, 2026 05:22
Use Object.defineProperty with value descriptors instead of getter
functions for const exports, reducing runtime overhead. Circular
modules are detected via iterative SCC and excluded from the
optimization. Gated behind optimization.inlineExports.
@xiaoxiaojx xiaoxiaojx force-pushed the feat/static-export-bindings branch from 44012f0 to ed29f43 Compare May 28, 2026 07:34
@github-actions

Copy link
Copy Markdown
Contributor

Types Coverage

Coverage after merging feat/static-export-bindings 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.48%100%100%98.48%1576, 1872, 1879, 1887, 1909, 2805, 3230, 3894, 3923, 3976–3977, 3981, 3986, 4002–4003, 4017–4018, 4023–4024, 4501, 4527, 511, 516, 5335, 5367, 5384, 5400, 5416, 5431, 5456–5457, 5459, 5787, 5792, 5798, 5801, 5813, 5815, 5819, 5835, 5850, 5882, 5936, 5960, 6074, 730–731
   Compiler.js99.56%100%100%99.56%1135–1136, 1144
   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.js99%100%100%99%170–171, 187, 206, 280
   DependenciesBlock.js100%100%100%100%
   Dependency.js98.20%100%100%98.20%384, 430
   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.85%100%100%98.85%434, 436, 440
   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.49%100%100%98.49%1304, 1309, 1369, 1383, 1445, 1454
   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%659
   MultiStats.js100%100%100%100%
   MultiWatching.js100%100%100%100%
   NoEmitOnErrorsPlugin.js100%100%100%100%
   NodeStuffPlugin.js100%100%100%100%
   NormalModule.js98.12%100%100%98.12%1212, 1215, 1232, 1249, 1496, 1530, 1546, 1633, 2257, 2262–2272, 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%326
   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.30%100%100%99.30%1427–1429, 1437, 272, 275, 280, 284
   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%
   

@alexander-akait alexander-akait merged commit 96e8c07 into main May 28, 2026
61 checks passed
@alexander-akait alexander-akait deleted the feat/static-export-bindings branch May 28, 2026 13:08
@bworline

Copy link
Copy Markdown
Contributor

@alexander-akait, @xiaoxiaojx

Optimization rules:
Only const declarations are eligible (function/class names can be reassigned in sloppy mode)

I'm not sure what 'sloppy mode' is, but (1) can this be toggled on/off with a webpack config setting, and (2) can you detect that it is off and so enable function and class names in this optimization? I feel that many more exports would fall into the class/function buckets and could benefit from this optimization.

@xiaoxiaojx

Copy link
Copy Markdown
Member Author

Good question! The "sloppy mode" comment in the code is actually slightly misleading — I'll fix that. The real reason is that function and class declarations create mutable bindings (like let), which can be reassigned even in strict mode (and ESM is always strict):

function fn() { return 1; }
fn = 2; // valid in strict mode
export { fn }; // fn could be 2

Only const guarantees immutability at the language level, so it's the only safe case without additional analysis.

To answer your questions:

  1. Toggle: Yes, this optimization is gated behind optimization.inlineExports (defaults to true in production).
  2. Extending to function/class: Strict mode detection wouldn't help here since reassignment is valid in both modes. However, we could extend this in the future by statically analyzing whether a function/class name is never reassigned within the module — that would safely unlock value binding for those exports too.

@alexander-akait

Copy link
Copy Markdown
Member

@xiaoxiaojx Let's add a todo for this too in code/issue

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.

Performance Optimization - Live Bindings

3 participants