Skip to content

feat: use optional chaining in generated runtime code where supported#21186

Merged
alexander-akait merged 8 commits into
mainfrom
feat/optional-chaining-runtime
Jun 15, 2026
Merged

feat: use optional chaining in generated runtime code where supported#21186
alexander-akait merged 8 commits into
mainfrom
feat/optional-chaining-runtime

Conversation

@alexander-akait

Copy link
Copy Markdown
Member

Summary

output.environment.optionalChaining already exists, but the generated runtime code almost never used it. This adds a RuntimeTemplate.optionalChaining(object, access) helper that emits obj?.x when the target environment supports optional chaining and falls back to the existing obj && obj.x short-circuit otherwise, then applies it across the runtime modules where it is safe (chunk loading for jsonp/import/readFile/require/importScripts, async modules, css loading, module federation remote/share/consume, load-script, auto-public-path, and deferred namespaces). No related issue.

Only property-access guards and method calls whose receiver is always object/array/null/undefined are converted; if (x) guards where x can be a falsy non-nullish value (e.g. the 0 parent-callback sentinel in the jsonp callback) are intentionally left as-is, because optional chaining only short-circuits on null/undefined.

What kind of change does this PR introduce?

feat

Did you add tests for your changes?

No new tests — the change only affects generated runtime code, which is executed end-to-end by the existing HotTestCases and ConfigTestCases suites. The && fallback output is byte-identical to the previous code, and the ?. output is exercised by the default modern-target runs; both were verified against baseline (no new failures).

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?

n/a

Use of AI

AI was used: Claude (Claude Code) helped locate candidate sites, write the optionalChaining helper and the edits, and run/compare the test suites. A test-driven review caught and reverted two unsafe conversions before submission. All changes were reviewed by me.


Generated by Claude Code

Add a RuntimeTemplate.optionalChaining helper that emits optional
chaining when output.environment.optionalChaining is supported, and an
equivalent && short-circuit otherwise. Use it in the load-script,
auto-public-path and deferred-namespace runtime modules.
…sites

Use RuntimeTemplate.optionalChaining in the chunk-loading (jsonp, import,
readFile, require, importScripts), async-module, css-loading and module
federation (remote/share/consume) runtime modules.

Only convert property-access guards and method calls whose receiver is
always object/array/null/undefined; keep `if (x)` guards where x can be a
falsy non-nullish value (e.g. the `0` parent-callback sentinel), since
optional chaining only short-circuits on null/undefined.
@changeset-bot

changeset-bot Bot commented Jun 15, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 15c6491

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

@github-actions

github-actions Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

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

Install it locally:

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

@codecov

codecov Bot commented Jun 15, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 92.68%. Comparing base (b83041a) to head (15c6491).
⚠️ Report is 6 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main   #21186      +/-   ##
==========================================
- Coverage   92.68%   92.68%   -0.01%     
==========================================
  Files         587      588       +1     
  Lines       64011    64027      +16     
  Branches    17754    17760       +6     
==========================================
+ Hits        59328    59341      +13     
- Misses       4683     4686       +3     
Flag Coverage Δ
css-parsing 28.70% <0.00%> (-0.01%) ⬇️
html5lib 31.21% <0.00%> (-0.01%) ⬇️
integration 88.64% <66.66%> (-0.01%) ⬇️
test262 45.49% <66.66%> (-0.05%) ⬇️
unit 41.06% <83.33%> (+<0.01%) ⬆️

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

☔ View full report in Codecov by Harness.
📢 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.

Cover both the optional-chaining and && fallback output for property,
call, method and computed access forms.
@codspeed-hq

codspeed-hq Bot commented Jun 15, 2026

Copy link
Copy Markdown

Merging this PR will improve performance by ×2.2

⚡ 3 improved benchmarks
✅ 141 untouched benchmarks

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Memory benchmark "lodash", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 859.4 KB 125.5 KB ×6.8
Memory benchmark "asset-modules-inline", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 1.3 MB 1 MB +24.18%
Memory benchmark "devtool-source-map", scenario '{"name":"mode-production","mode":"production"}' 7.8 MB 6.4 MB +20.88%

Tip

Curious why this is faster? Comment @codspeedbot explain why this is faster on this PR, or directly use the CodSpeed MCP with your agent.


Comparing feat/optional-chaining-runtime (15c6491) with main (b04e811)

Open in CodSpeed

The optional-chaining output shrinks the publicPath runtime below 1 KiB,
so verbose/detailed stats now report it in bytes.
Optional-chaining output reduces the entry asset size by a few bytes.
The test harness executes generated bundles in the same Node.js process,
and Node < 14 cannot parse `?.`. Default output.environment.optionalChaining
to the running Node's capability in the Config/Watch/Test/Hot templates so
old Node gets the `&&` fallback while modern Node still exercises `?.`.
Those cases assert the target-derived output.environment, so forcing
optionalChaining off on Node < 14 broke their snapshots. Keep the Node
version guard for the other (execution-based) config cases.
It executes the federation bundle (with `promise?.then`) in-process via vm,
which Node < 14 can't parse; gate optionalChaining on the running Node.
@github-actions

Copy link
Copy Markdown
Contributor

Types Coverage

Coverage after merging feat/optional-chaining-runtime into main will be
99.34%
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.44%100%100%98.44%1609, 1905, 1912, 1920, 1942, 2838, 3317–3318, 3350, 4014, 4044, 4097–4098, 4102, 4107, 4123–4124, 4138–4139, 4144–4145, 4622, 4648, 513, 518, 5456, 5488, 5505, 5521, 5537, 5552, 5577–5578, 5580, 5908, 5913, 5919, 5922, 5934, 5936, 5940, 5956, 5971, 6003, 6057, 6081, 6195, 763–764
   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%171–172, 188, 207, 281
   DependenciesBlock.js100%100%100%100%
   Dependency.js98.15%100%100%98.15%379, 425
   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, 3693, 3708, 3732
   FlagAllModulesAsUsedPlugin.js100%100%100%100%
   FlagDependencyExportsPlugin.js98.46%100%100%98.46%425, 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.50%100%100%98.50%1285, 1290, 1350, 1364, 1426, 1435
   ModuleFactory.js100%100%100%100%
   ModuleFilenameHelpers.js98.85%100%100%98.85%106, 108
   ModuleGraph.js99.73%100%100%99.73%1005
   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.js97.90%100%100%97.90%1240, 1243, 1260, 1277, 1524, 1558, 1574, 1661, 2017, 2316, 2321–2331, 419, 423, 577
   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.js99.17%100%100%99.17%176–177
   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%371
   cli.js98.62%100%100%98.62%10, 119, 545, 577, 627, 897
   index.js99.72%100%100%99.72%179
   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%
   AssetModule.js100%100%100%100%
   AssetModulesPlugin.js97.32%100%100%97.32%281, 305, 308, 36, 360, 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%1439–1441, 1449, 274, 277, 282, 286
   normalization.js99.01%100%100%99.01%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%

@alexander-akait alexander-akait merged commit 793aae6 into main Jun 15, 2026
66 checks passed
@alexander-akait alexander-akait deleted the feat/optional-chaining-runtime branch June 15, 2026 13:09
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.

1 participant