Skip to content

perf(serialization): reduce allocations in binary middleware hot paths - #21526

Merged
alexander-akait merged 1 commit into
mainfrom
perf/binary-middleware-followup
Jul 27, 2026
Merged

perf(serialization): reduce allocations in binary middleware hot paths#21526
alexander-akait merged 1 commit into
mainfrom
perf/binary-middleware-followup

Conversation

@alexander-akait

Copy link
Copy Markdown
Member

Summary

Follow-up to the V8 serializer rewrite (#21514). That path is bound by native v8.serialize/v8.deserialize (benchmarking shows ~99% of deserialize is the native call and the JS framing is <1%), so this is not a wall-clock change — it just trims avoidable allocations on the hot path, in the spirit of #21516. _deserialize allocated an intermediate parts array and made a second concatenation pass even for the dominant single-section stream; it now folds accumulation into the read loop (the first section's array becomes the result, later sections append in place). The full-array buffer null-out also switches from a for…of iterator to an indexed loop to match the restore loop beside it. No behavior change. Refs #21514.

What kind of change does this PR introduce?

perf

Did you add tests for your changes?

No — there is no behavior change; the existing test/BinaryMiddleware.unittest.js round-trip suite already covers the single-section, multi-section split, lazy, and frozen/buffer paths this touches.

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 (Claude) was used to analyze the merged #21514 serializer for memory/performance, benchmark it (isolating native V8 cost from JS framing), and implement these allocation-hygiene cleanups; all changes were reviewed before submission.


Generated by Claude Code

Fold _deserialize's section accumulation into the read loop so a
single-section stream no longer allocates an intermediate parts array
or makes a second pass; use an indexed loop for the full-array buffer
null-out to drop an iterator allocation.
@changeset-bot

changeset-bot Bot commented Jul 27, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 43e6849

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

@cursor

cursor Bot commented Jul 27, 2026

Copy link
Copy Markdown

PR Summary

Low Risk
Refactor-only allocation tweaks in serialization framing with no intended behavior change; existing round-trip tests cover the touched paths.

Overview
BinaryMiddleware allocation hygiene on serialize/deserialize framing (follow-up to the V8 serializer work; no semantic change).

_deserialize no longer builds a parts array and a second merge pass. The first section’s array becomes result; additional sections append in place. Empty streams still return [].

Serialize flush when nulling buffer slots in the in-place path uses an indexed for loop instead of for…of over bufferIndices, matching the restore loop beside it.

Changeset: patch note for reduced allocations on binary serialization hot paths.

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

@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

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

Install it locally:

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

@github-actions

Copy link
Copy Markdown
Contributor

Types Coverage

Coverage after merging perf/binary-middleware-followup into main will be
99.32%
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.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.12%100%100%99.12%207, 227
   CodeGenerationResults.js100%100%100%100%
   CompatibilityPlugin.js100%100%100%100%
   Compilation.js98.42%100%100%98.42%1639, 1958, 1965, 1973, 1995, 1998, 2937, 3416–3417, 3449, 4149, 4179, 4232–4233, 4237, 4242, 4258–4259, 4273–4274, 4279–4280, 4760, 4786, 527, 532, 5594, 5626, 5643, 5659, 5675, 5690, 5715–5716, 5718, 6048, 6053, 6059, 6062, 6069, 6081, 6083, 6087, 6103, 6118, 6150, 6204, 6228, 6343, 778–779
   Compiler.js99.56%100%100%99.56%1147–1148, 1156
   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%1074, 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%1196, 1199, 514–518, 520, 666
   ExternalModuleFactoryPlugin.js100%100%100%100%
   ExternalsPlugin.js100%100%100%100%
   FileSystemInfo.js99.52%100%100%99.52%182, 2402–2403, 2406, 2417, 2428, 2439, 280, 3876, 3891, 3915
   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.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.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.98%100%100%97.98%1014, 1031, 1279, 1313, 1329, 1776, 2073, 2078–2088, 34, 994, 997
   NormalModuleFactory.js98.72%100%100%98.72%1117, 1385, 1396, 1406, 1457–1459, 1466, 520, 532
   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.41%100%100%96.41%1257, 1357, 1361, 1423, 628, 647, 657–659, 661, 677–678, 683, 686, 688, 693, 698, 723, 729, 763, 769, 775, 780, 791, 800, 805–806, 808, 825, 831–832, 834
   ResolverCachePlugin.js100%100%100%100%
   getLazyHashedEtag.js100%100%100%100%
   mergeEtags.js100%100%100%100%
lib/config
   browserslistTargetHandler.js100%100%100%100%
   defaults.js99.37%100%100%99.37%1620–1622, 1630,

@codecov

codecov Bot commented Jul 27, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.79%. Comparing base (84d351c) to head (43e6849).
⚠️ Report is 5 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main   #21526      +/-   ##
==========================================
+ Coverage   93.78%   93.79%   +0.01%     
==========================================
  Files         620      620              
  Lines       73371    73370       -1     
  Branches    21192    21196       +4     
==========================================
+ Hits        68811    68818       +7     
+ Misses       4560     4552       -8     
Flag Coverage Δ
css-parsing 25.53% <ø> (ø)
html5lib 26.96% <ø> (ø)
integration 89.60% <100.00%> (+0.01%) ⬆️
test262 43.27% <ø> (-0.03%) ⬇️
unit 47.15% <100.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.

@codspeed-hq

codspeed-hq Bot commented Jul 27, 2026

Copy link
Copy Markdown

Merging this PR will regress 1 benchmark

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 3 improved benchmarks
❌ 1 regressed benchmark
✅ 212 untouched benchmarks

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Memory benchmark "asset-modules-resource", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 635.8 KB 1,209.3 KB -47.42%
Memory benchmark "future-defaults", scenario '{"name":"mode-production","mode":"production"}' 10.6 MB 7.7 MB +37.54%
Memory benchmark "many-chunks-commonjs", scenario '{"name":"mode-production","mode":"production"}' 8.5 MB 7 MB +21.72%
Simulation benchmark "cache-filesystem", scenario '{"name":"mode-development","mode":"development"}' 1.3 s 1.1 s +20.58%

Tip

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


Comparing perf/binary-middleware-followup (43e6849) with main (84d351c)1

Open in CodSpeed

Footnotes

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

Copy link
Copy Markdown
Member Author

On the CodSpeed memory regression (asset-modules-resource / mode-development-rebuild): I don't think it's real or actionable for this diff.

  • This change only removes allocations — it drops the intermediate parts array in _deserialize and replaces one for…of with an indexed loop. It adds no retained state and no new allocation, so there's no mechanism by which it could raise peak heap by ~570 KB in a rebuild scenario; if anything it's neutral-to-lower on memory.
  • The report flags two reliability caveats on exactly this comparison — "Different runtime environments detected … may affect the accuracy" and "No successful run was found on main (4efcd73) … 84d351c was used instead … there might be some changes unrelated to this pull request" — and the regressed case is a single high-variance Memory watch-rebuild benchmark.
  • The same run shows two memory benchmarks improving (future-defaults +37.5%, many-chunks-commonjs +21.7%), which is inconsistent with a real regression from an allocation-reducing change.

I read this as environment/base-substitution noise rather than a regression introduced here, so I don't plan to change the code for it. Happy to re-trigger the benchmark or acknowledge it on CodSpeed if preferred.


Generated by Claude Code

Copy link
Copy Markdown
Member Author

The runtimes (bun) failure is unrelated to this change and not code-fixable here.

The failing suites are HotTestCases, and the error is in the HMR lazy-compilation HTTP server teardown — lib/hmr/lazyCompilationBackend.js:181 throwing "Server is not running" from server.close() in dispose (2 failed tests across 6 suites, 94,139 passed). That's a Bun HTTP-server lifecycle/teardown issue in a subsystem this PR doesn't touch — the diff only changes lib/serialization/BinaryMiddleware.js (persistent-cache deserialize framing), which has no path to an HMR server's close behavior. HotTestCases use the in-memory cache, not the filesystem serializer, so the deserialize change isn't even exercised here.

I'll re-trigger the Bun job to clear the flake; I'm not changing the diff for it. If it reproduces, it's a pre-existing Bun/lazyCompilationBackend teardown issue on main rather than something introduced here.


Generated by Claude Code

@alexander-akait
alexander-akait merged commit aa1e746 into main Jul 27, 2026
63 of 64 checks passed
@alexander-akait
alexander-akait deleted the perf/binary-middleware-followup branch July 27, 2026 01:24
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