Skip to content

Add output.resourceHints.dedupe to skip prefetch for already preloaded/prefetched chunks - #21386

Merged
alexander-akait merged 1 commit into
mainfrom
feat/output-dedupe-prefetch
Jul 30, 2026
Merged

Add output.resourceHints.dedupe to skip prefetch for already preloaded/prefetched chunks#21386
alexander-akait merged 1 commit into
mainfrom
feat/output-dedupe-prefetch

Conversation

@alexander-akait

@alexander-akait alexander-akait commented Jul 11, 2026

Copy link
Copy Markdown
Member

Summary

Some browsers (Chrome) fire a second network request when a <link rel="prefetch"> is inserted for a resource that was already hinted via markup (e.g. an SSR/HTML-injected <link rel="preload">). This adds an opt-in output.resourceHints.dedupe (default false) — a new boolean on the existing output.resourceHints group — that makes the JSONP and ESM chunk-loading runtimes scan existing <link> elements and skip the prefetch when a matching preload/prefetch already exists. webpack already dedupes its own preload→prefetch via installedChunks, so this only covers externally-authored hints; keeping it opt-in means builds that don't need it pay no runtime cost. Refs #17496.

What kind of change does this PR introduce?

feat — a new opt-in output.resourceHints sub-option.

Did you add tests for your changes?

Yes — test/configCases/web/prefetch-preload-dedupe (classic/JSONP output) and test/configCases/web/prefetch-preload-dedupe-module (ESM output), each asserting the already-preloaded chunk is skipped while others still prefetch; plus updated Defaults, and CLI-flags snapshots.

Does this PR introduce a breaking change?

No — the option defaults to false, so existing builds are unchanged.

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

The new output.resourceHints.dedupe boolean should be documented alongside the other output.resourceHints options on webpack.js.org.

Use of AI

AI (Claude) was used to reproduce the double-request in Chromium, implement the option across schema/defaults/runtime, write the tests, and regenerate the types/validators. All changes were reviewed and verified locally (targeted Jest suites + tsc).


Note

Low Risk
Opt-in config defaulting to false; behavior change is limited to browser prefetch injection when dedupe is explicitly enabled.

Overview
Adds output.resourceHints.dedupe (default false) on the existing output.resourceHints object. When enabled, the JSONP and ESM chunk-loading runtimes scan existing <link> elements before appending a runtime <link rel="prefetch"> and bail out if the same href is already covered by rel="prefetch", preload, or modulepreload—addressing duplicate fetches in Chrome when SSR/HTML already injected preload hints.

Wiring includes defaults (lib/config/defaults.js), generated TypeScript declarations, a minor changeset, and an AGENTS.md note that runtime-emitting code must stay consistent across chunk-loading targets.

Reviewed by Cursor Bugbot for commit debdbea. Bugbot is set up for automated code reviews on this repo. Configure here.

Copilot AI review requested due to automatic review settings July 11, 2026 13:22
@changeset-bot

changeset-bot Bot commented Jul 11, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: debdbea

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions

github-actions Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

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

Install it locally:

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

@codspeed-hq

codspeed-hq Bot commented Jul 11, 2026

Copy link
Copy Markdown

Hooray! CodSpeed harness just leveled up!

The base and head of this comparison were measured with different runner settings, so their benchmark values are not directly comparable.

What changed between base and head:

  • CodSpeed runner v5.0.0 changed how benchmarks are measured (base 4.18.4 → head 5.0.1). View release notes

Re-run the base with the same settings to get a valid performance comparison.


Comparing feat/output-dedupe-prefetch (ab9b713) with main (13c83e6)1

Open in CodSpeed

Footnotes

  1. No successful run was found on main (c20c1a6) during the generation of this report, so 13c83e6 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@codecov

codecov Bot commented Jul 11, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.91%. Comparing base (c20c1a6) to head (debdbea).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main   #21386   +/-   ##
=======================================
  Coverage   93.91%   93.91%           
=======================================
  Files         621      621           
  Lines       73867    73874    +7     
  Branches    21372    21378    +6     
=======================================
+ Hits        69375    69382    +7     
  Misses       4492     4492           
Flag Coverage Δ
css-parsing 25.51% <0.00%> (-0.01%) ⬇️
html5lib 26.93% <0.00%> (-0.01%) ⬇️
integration 89.90% <100.00%> (+<0.01%) ⬆️
test262 43.17% <28.57%> (-0.04%) ⬇️
unit 47.05% <25.00%> (-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.

@alexander-akait
alexander-akait force-pushed the feat/output-dedupe-prefetch branch from 6375957 to 7008911 Compare July 11, 2026 13:39
Copilot AI review requested due to automatic review settings July 11, 2026 13:39

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@alexander-akait
alexander-akait force-pushed the feat/output-dedupe-prefetch branch from 7008911 to 8aece90 Compare July 30, 2026 10:34
@alexander-akait alexander-akait changed the title Add output.dedupePrefetch to skip prefetch for already preloaded/prefetched chunks Add output.resourceHints.dedupe to skip prefetch for already preloaded/prefetched chunks Jul 30, 2026

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 8aece90. Configure here.

Comment thread lib/esm/ModuleChunkLoadingRuntimeModule.js Outdated
@alexander-akait
alexander-akait force-pushed the feat/output-dedupe-prefetch branch from 8aece90 to ab9b713 Compare July 30, 2026 10:38
…hunks

Some browsers (Chrome) issue a second network request when a
<link rel="prefetch"> is inserted for a resource already hinted via
markup. The new opt-in output.resourceHints.dedupe flag makes the JSONP
and ESM chunk-loading runtimes scan existing <link> elements and skip the
prefetch when a matching preload/prefetch already exists. Default false,
so builds that do not need it pay no runtime cost.

Refs #17496
@alexander-akait
alexander-akait force-pushed the feat/output-dedupe-prefetch branch from ab9b713 to debdbea Compare July 30, 2026 10:50
@github-actions

Copy link
Copy Markdown
Contributor

Types Coverage

Coverage after merging feat/output-dedupe-prefetch into main will be
99.32%
Coverage Report<
FileStmtsBranchesFuncsLinesUncovered Lines
bin
   webpack.js98.82%100%100%98.82%103
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.js100%100%100%100%
examples/markdown
   webpack.config.mjs100%100%100%100%
examples/module-federation
   test.filter.js100%100%100%100%
examples/reexport-components
   test.filter.js100%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-emscripten
   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%
   CircularModulesPlugin.js98.81%100%100%98.81%136
   CleanPlugin.js99.13%100%100%99.13%214, 234
   CodeGenerationResults.js100%100%100%100%
   CompatibilityPlugin.js100%100%100%100%
   Compilation.js98.42%100%100%98.42%1648, 1967, 1974, 1982, 2004, 2007, 2946, 3425–3426, 3458, 4162, 4192, 4245–4246, 4250, 4255, 4271–4272, 4286–4287, 4292–4293, 4773, 4799, 527, 532, 5607, 5639, 5656, 5672, 5688, 5703, 5728–5729, 5731, 6061, 6066, 6072, 6075, 6082, 6094, 6096, 6100, 6116, 6131, 6163, 6217, 6241, 6356, 778–779
   Compiler.js99.56%100%100%99.56%1159–1160, 1168
   ConcatenationScope.js98.65%100%100%98.65%195
   ConditionalInitFragment.js100%100%100%100%
   ConstPlugin.js100%100%100%100%
   ContextExclusionPlugin.js100%100%100%100%
   ContextModule.js99.88%100%100%99.88%1461
   ContextModuleFactory.js97.20%100%100%97.20%266, 435, 456, 461, 501, 512, 514, 518, 527–528
   ContextReplacementPlugin.js100%100%100%100%
   DefinePlugin.js99.08%100%100%99.08%1080, 176–177, 193, 212, 286
   DependenciesBlock.js100%100%100%100%
   Dependency.js98.51%100%100%98.51%479, 525
   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.65%100%100%98.65%1202, 1205, 514–518, 520, 666
   ExternalModuleFactoryPlugin.js100%100%100%100%
   ExternalsPlugin.js100%100%100%100%
   FileSystemInfo.js99.16%100%100%99.16%1267, 1269–1274, 1281, 1284, 182, 2502–2503, 2506, 2517, 2528, 2539, 280, 3976, 3991, 4015
   FlagAllModulesAsUsedPlugin.js100%100%100%100%
   FlagDependencyExportsPlugin.js98.21%100%100%98.21%448, 457, 460, 464, 476
   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%
   LazyBarrel.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%1288, 1293, 1353, 1367, 1429, 1438
   ModuleFactory.js100%100%100%100%
   ModuleFilenameHelpers.js98.89%100%100%98.89%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.70%100%100%99.70%663
   MultiStats.js100%100%100%100%
   MultiWatching.js100%100%100%100%
   NoEmitOnErrorsPlugin.js100%100%100%100%
   NodeStuffPlugin.js100%100%100%100%
   NormalModule.js97.96%100%100%97.96%1000, 1017, 1034, 1282, 1316, 1332, 1779, 2076, 2081–2091, 34, 997
   NormalModuleFactory.js98.72%100%100%98.72%1123, 1391, 1402, 1412, 1463–1465, 1472, 526, 538
   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.js99.80%100%100%99.80%691
   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, 889
   Stats.js100%100%100%100%
   Template.js100%100%100%100%
   TemplatedPathPlugin.js99.43%100%100%99.43%308–309
   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.63%100%100%98.63%10, 119, 549, 581, 631, 905
   index.js99.72%100%100%99.72%184
   validateSchema.js94.67%100%100%94.67%100, 87, 89, 98
   webpack.js97.10%100%100%97.10%10, 263, 285, 287
lib/asset
   AssetBytesGenerator.js100%100%100%100%
   AssetBytesParser.js100%100%100%100%
   AssetGenerator.js100%100%100%100%
   AssetModule.js100%100%100%100%
   AssetModulesPlugin.js97.95%100%100%97.95%295, 319, 322, 42, 452, 47
   AssetParser.js100%100%100%100%
   AssetSourceGenerator.js100%100%100%100%
   AssetSourceParser.js100%100%100%100%
   RawDataUrlModule.js100%100%100%100%
   WebManifestGenerator.js100%100%100%100%
   WebManifestParser.js100%100%100%100%
lib/async-modules
   AsyncModuleHelpers.js100%100%100%100%
   AwaitDependenciesInitFragment.js100%100%100%100%
   InferAsyncModulesPlugin.js100%100%100%100%
   isGeneratorLowered.js100%100%100%100%
lib/bun
   BunTargetPlugin.js100%100%100%100%
lib/cache
   AddBuildDependenciesPlugin.js100%100%100%100%
   AddManagedPathsPlugin.js100%100%100%100%
   IdleFileCachePlugin.js97.92%100%100%97.92%75, 87, 95
   MemoryCachePlugin.js95.83%100%100%95.83%33
   MemoryWithGcCachePlugin.js93.15%100%100%93.15%107, 114–115, 123, 90
   PackFileCacheStrategy.js96.52%100%100%96.52%1310, 1410, 1414, 1476, 1712, 1796, 1819, 1851, 675, 694, 704–706, 708, 724–725, 730, 733, 735, 740, 745, 770, 776, 810, 816, 822, 827, 838, 847, 852–853, 855, 872, 878–879, 881
   ResolverCachePlugin.js100%100%100%100%
   getLazyHashedEtag.js100%100%100%

@alexander-akait
alexander-akait merged commit ba6e94b into main Jul 30, 2026
60 of 62 checks passed
@alexander-akait
alexander-akait deleted the feat/output-dedupe-prefetch branch July 30, 2026 11:15
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

Sponsor
SponsoredKunjungi sekarang
Promo