Skip to content

feat(chunk-loading): bypass Promise.all for single sync-then chunk loads#2597

Merged
upupming merged 7 commits into
mainfrom
feat/sync-then-chunk-load
May 11, 2026
Merged

feat(chunk-loading): bypass Promise.all for single sync-then chunk loads#2597
upupming merged 7 commits into
mainfrom
feat/sync-then-chunk-load

Conversation

@upupming
Copy link
Copy Markdown
Collaborator

@upupming upupming commented May 11, 2026

Summary

__webpack_require__.e always wrapped chunk handlers' promises in Promise.all, which collapses any sync-then promise back into a native one. preact's lazy(loader) therefore always saw a pending `prom` at first render and fell through to the Suspense fallback — even when the underlying `lynx.loadLazyBundle` resolved synchronously (cached / `fetchBundle.wait()`).

Override `e` from the Lynx chunk-loading runtime so that when exactly one handler pushes exactly one promise (the typical lazy-bundle shape — one JS chunk, no separate CSS chunk, no shared splits), we return that promise directly. The sync-then contract survives all the way to preact and the lazy component renders on the first pass.

Length 0 and length > 1 fall through to the original `Promise.all` path; behavior for those is unchanged.

Test plan

  • Existing `@lynx-js/chunk-loading-webpack-plugin` tests still pass (40/40)
  • Existing `@lynx-js/react-runtime` tests still pass
  • Built `examples/react-lazy-bundle` and confirmed the runtime now emits `r.e=function(e){var t=[];return(...,1===t.length)?t[0]:Promise.all(t)}` instead of always-Promise.all
  • Manual: lazy bundle renders without Suspense fallback flash on cached load

Summary by CodeRabbit

  • Bug Fixes

    • Single-chunk dynamic imports now take a fast synchronous path so lazy-loaded components resolve immediately in common cases; multi-chunk behavior remains unchanged.
  • Tests

    • Added tests covering single-vs-multi-chunk promise behavior and end-to-end dynamic import scenarios.
  • Chores

    • Bumped chunk-loading plugin to a patch release.

Review Change Stack

@upupming upupming requested review from colinaaa and luhc228 as code owners May 11, 2026 08:55
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 11, 2026

🦋 Changeset detected

Latest commit: 5abf278

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

This PR includes changesets to release 4 packages
Name Type
@lynx-js/chunk-loading-webpack-plugin Patch
@lynx-js/rspeedy Patch
create-rspeedy Patch
upgrade-rspeedy 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

@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 11, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6be4b24c-a729-46d1-842c-0af2351ff57f

📥 Commits

Reviewing files that changed from the base of the PR and between 5e7066c and 5abf278.

📒 Files selected for processing (5)
  • .changeset/sync-then-chunk-load.md
  • packages/webpack/chunk-loading-webpack-plugin/src/runtime/javascript/chunk-loading.js
  • packages/webpack/chunk-loading-webpack-plugin/test/cases/chunk-loading/ensure-chunk-shortcut/dynamic.js
  • packages/webpack/chunk-loading-webpack-plugin/test/cases/chunk-loading/ensure-chunk-shortcut/index.js
  • packages/webpack/chunk-loading-webpack-plugin/test/cases/chunk-loading/ensure-chunk-shortcut/rspack.config.js
✅ Files skipped from review due to trivial changes (2)
  • packages/webpack/chunk-loading-webpack-plugin/test/cases/chunk-loading/ensure-chunk-shortcut/dynamic.js
  • .changeset/sync-then-chunk-load.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/webpack/chunk-loading-webpack-plugin/src/runtime/javascript/chunk-loading.js
  • packages/webpack/chunk-loading-webpack-plugin/test/cases/chunk-loading/ensure-chunk-shortcut/index.js
  • packages/webpack/chunk-loading-webpack-plugin/test/cases/chunk-loading/ensure-chunk-shortcut/rspack.config.js

📝 Walkthrough

Walkthrough

A patch release adds a $RuntimeGlobals_ensureChunk$ helper that collects per-handler promises and returns the single handler promise directly (preserving sync-then resolution) or Promise.all(promises) when multiple handlers contribute; includes tests, a test build config, and a changeset.

Changes

Sync-then chunk load optimization

Layer / File(s) Summary
Chunk loading runtime helper
packages/webpack/chunk-loading-webpack-plugin/src/runtime/javascript/chunk-loading.js
Adds $RuntimeGlobals_ensureChunk$ that iterates registered handlers, collects promises, and returns a single promise directly (single handler) or Promise.all(promises) when multiple handlers return promises.
Test helpers & mocks
packages/webpack/chunk-loading-webpack-plugin/test/cases/chunk-loading/ensure-chunk-shortcut/index.js
Adds SYNC_MARKER, makeSyncThenPromise, and globalThis.lynx test doubles (loadLazyBundle, requireModuleAsync).
Single-handler preserve test
packages/webpack/chunk-loading-webpack-plugin/test/cases/chunk-loading/ensure-chunk-shortcut/index.js
Asserts __webpack_require__.e(chunkId) preserves the sync-then then marker when only one handler contributes a promise.
Multi-handler Promise.all behavior test
packages/webpack/chunk-loading-webpack-plugin/test/cases/chunk-loading/ensure-chunk-shortcut/index.js
Injects multiple pushed promises and asserts the returned value uses Promise.all (no SYNC_MARKER) and resolves to an array of results.
End-to-end dynamic import test
packages/webpack/chunk-loading-webpack-plugin/test/cases/chunk-loading/ensure-chunk-shortcut/index.js, packages/webpack/chunk-loading-webpack-plugin/test/cases/chunk-loading/ensure-chunk-shortcut/dynamic.js
Performs a dynamic import of ./dynamic.js, verifies __webpack_require__.lynx_aci registration and that lynx.loadLazyBundle was invoked.
Test build/runtime injection config
packages/webpack/chunk-loading-webpack-plugin/test/cases/chunk-loading/ensure-chunk-shortcut/rspack.config.js
Rspack config enabling chunkLoading: 'lynx', CommonJS chunk outputs, registers ChunkLoadingWebpackPlugin, and injects a runtime module to set RuntimeGlobals.lynxAsyncChunkIds.
Release documentation
.changeset/sync-then-chunk-load.md
Changeset bumping @lynx-js/chunk-loading-webpack-plugin and documenting that __webpack_require__.e now avoids Promise.all for single sync-then chunk loads while preserving multi-promise behavior.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • colinaaa
  • luhc228

Poem

🐇 I hop through bundles, light and fleet,
One promise lands and keeps the beat,
When others swarm they form an all,
But single steps stay sync and small,
A rabbit smiles at timing sweet.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(chunk-loading): bypass Promise.all for single sync-then chunk loads' directly and specifically describes the main change—a new optimization that skips Promise.all wrapping for single-promise chunk loads to preserve sync-then behavior.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/sync-then-chunk-load

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cla-assistant
Copy link
Copy Markdown

cla-assistant Bot commented May 11, 2026

CLA assistant check
All committers have signed the CLA.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 11, 2026

Codecov Report

❌ Patch coverage is 0% with 17 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
...ack-plugin/src/runtime/javascript/chunk-loading.js 0.00% 17 Missing ⚠️

📢 Thoughts on this report? Let us know!

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
packages/webpack/chunk-loading-webpack-plugin/test/cases/chunk-loading/ensure-chunk-shortcut/index.js (1)

28-32: 💤 Low value

Consider wrapping require() in error handling.

The loadLazyBundle mock calls require() directly on line 30, which will throw synchronously if the bundle file is missing or fails to parse. While this is acceptable in test code (failing fast when fixtures are broken), wrapping it in a try-catch and returning a rejected promise would make the mock more robust and consistent with typical async error handling.

♻️ Optional: Add error handling
   loadLazyBundle: rstest.fn(function loadLazyBundle(request) {
+    try {
-      return makeSyncThenPromise(
-        require(path.join(__dirname, `${request}.rspack.bundle.cjs`)),
-      );
+      return makeSyncThenPromise(
+        require(path.join(__dirname, `${request}.rspack.bundle.cjs`)),
+      );
+    } catch (error) {
+      return Promise.reject(error);
+    }
   }),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/webpack/chunk-loading-webpack-plugin/test/cases/chunk-loading/ensure-chunk-shortcut/index.js`
around lines 28 - 32, The mock loadLazyBundle function currently calls
require(path.join(__dirname, `${request}.rspack.bundle.cjs`)) synchronously
which will throw on missing or invalid bundles; modify the loadLazyBundle (the
rstest.fn named loadLazyBundle) to wrap the require call in a try-catch and, on
error, return a rejected promise (or pass the error into makeSyncThenPromise as
a rejection) so consumers receive an async rejection instead of an uncaught
synchronous throw.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In
`@packages/webpack/chunk-loading-webpack-plugin/test/cases/chunk-loading/ensure-chunk-shortcut/index.js`:
- Around line 28-32: The mock loadLazyBundle function currently calls
require(path.join(__dirname, `${request}.rspack.bundle.cjs`)) synchronously
which will throw on missing or invalid bundles; modify the loadLazyBundle (the
rstest.fn named loadLazyBundle) to wrap the require call in a try-catch and, on
error, return a rejected promise (or pass the error into makeSyncThenPromise as
a rejection) so consumers receive an async rejection instead of an uncaught
synchronous throw.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: dcf4e2a0-00ea-4035-bf7e-b6efe78917ae

📥 Commits

Reviewing files that changed from the base of the PR and between 1b96083 and 6aafb0a.

📒 Files selected for processing (3)
  • packages/webpack/chunk-loading-webpack-plugin/test/cases/chunk-loading/ensure-chunk-shortcut/dynamic.js
  • packages/webpack/chunk-loading-webpack-plugin/test/cases/chunk-loading/ensure-chunk-shortcut/index.js
  • packages/webpack/chunk-loading-webpack-plugin/test/cases/chunk-loading/ensure-chunk-shortcut/rspack.config.js
✅ Files skipped from review due to trivial changes (1)
  • packages/webpack/chunk-loading-webpack-plugin/test/cases/chunk-loading/ensure-chunk-shortcut/dynamic.js

upupming added 7 commits May 11, 2026 17:59
`__webpack_require__.e` always wrapped chunk handlers in `Promise.all`,
which collapses any sync-then promise back into a native Promise. As a
result, preact's `lazy(loader)` always saw a pending `prom` at first
render and fell through to Suspense fallback, even when the underlying
`lynx.loadLazyBundle` resolved synchronously (cached / fetchBundle wait).

Override `e` from the Lynx chunk-loading runtime: when exactly one
handler pushed exactly one promise into the array — the dominant
lazy-bundle shape — return that promise directly so the sync-then
contract survives all the way to preact. Length 0 and length > 1 fall
through to the original Promise.all path.
Use the same Object.keys(...).reduce(...) pattern that rspack's default
__webpack_require__.e emits, so the overridden version differs only in
the final Promise.all-vs-shortcut decision.
Mirror the existing runtime-globals case shape: mock lynx.loadLazyBundle
so it returns a promise carrying a sync-then marker, then assert that
__webpack_require__.e preserves the marker on a single-promise load and
strips it (falling back to native Promise.all) when multiple handlers
push promises.
@upupming upupming force-pushed the feat/sync-then-chunk-load branch from 5e7066c to 5abf278 Compare May 11, 2026 09:59
@upupming upupming enabled auto-merge (squash) May 11, 2026 10:05
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented May 11, 2026

Merging this PR will degrade performance by 16.18%

❌ 1 regressed benchmark
✅ 80 untouched benchmarks
⏩ 26 skipped benchmarks1

⚠️ Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
008-many-use-state-destroyBackground 8 ms 9.5 ms -16.18%

Comparing feat/sync-then-chunk-load (5abf278) with main (d588d03)

Open in CodSpeed

Footnotes

  1. 26 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.

@upupming upupming merged commit ad1f90f into main May 11, 2026
39 of 42 checks passed
@upupming upupming deleted the feat/sync-then-chunk-load branch May 11, 2026 10:20
@relativeci
Copy link
Copy Markdown

relativeci Bot commented May 11, 2026

React External

#1119 Bundle Size — 690.27KiB (0%).

5abf278(current) vs d588d03 main#1112(baseline)

Bundle metrics  no changes
                 Current
#1119
     Baseline
#1112
No change  Initial JS 0B 0B
No change  Initial CSS 0B 0B
No change  Cache Invalidation 0% 0%
No change  Chunks 0 0
No change  Assets 3 3
No change  Modules 17 17
No change  Duplicate Modules 5 5
No change  Duplicate Code 8.59% 8.59%
No change  Packages 0 0
No change  Duplicate Packages 0 0
Bundle size by type  no changes
                 Current
#1119
     Baseline
#1112
No change  Other 690.27KiB 690.27KiB

Bundle analysis reportBranch feat/sync-then-chunk-loadProject dashboard


Generated by RelativeCIDocumentationReport issue

@relativeci
Copy link
Copy Markdown

relativeci Bot commented May 11, 2026

React Example with Element Template

#270 Bundle Size — 197.79KiB (0%).

5abf278(current) vs d588d03 main#263(baseline)

Bundle metrics  Change 2 changes
                 Current
#270
     Baseline
#263
No change  Initial JS 0B 0B
No change  Initial CSS 0B 0B
No change  Cache Invalidation 0% 0%
No change  Chunks 0 0
No change  Assets 4 4
Change  Modules 81(+2.53%) 79
No change  Duplicate Modules 23 23
Change  Duplicate Code 40.29%(-0.1%) 40.33%
No change  Packages 2 2
No change  Duplicate Packages 0 0
Bundle size by type  no changes
                 Current
#270
     Baseline
#263
No change  IMG 145.76KiB 145.76KiB
No change  Other 52.03KiB 52.03KiB

Bundle analysis reportBranch feat/sync-then-chunk-loadProject dashboard


Generated by RelativeCIDocumentationReport issue

@relativeci
Copy link
Copy Markdown

relativeci Bot commented May 11, 2026

React MTF Example

#1135 Bundle Size — 206.6KiB (0%).

5abf278(current) vs d588d03 main#1128(baseline)

Bundle metrics  no changes
                 Current
#1135
     Baseline
#1128
No change  Initial JS 0B 0B
No change  Initial CSS 0B 0B
No change  Cache Invalidation 0% 0%
No change  Chunks 0 0
No change  Assets 3 3
No change  Modules 192 192
No change  Duplicate Modules 77 77
No change  Duplicate Code 44.36% 44.36%
No change  Packages 2 2
No change  Duplicate Packages 0 0
Bundle size by type  no changes
                 Current
#1135
     Baseline
#1128
No change  IMG 111.23KiB 111.23KiB
No change  Other 95.37KiB 95.37KiB

Bundle analysis reportBranch feat/sync-then-chunk-loadProject dashboard


Generated by RelativeCIDocumentationReport issue

@relativeci
Copy link
Copy Markdown

relativeci Bot commented May 11, 2026

Web Explorer

#9577 Bundle Size — 900.04KiB (0%).

5abf278(current) vs d588d03 main#9570(baseline)

Bundle metrics  Change 1 change
                 Current
#9577
     Baseline
#9570
No change  Initial JS 44.46KiB 44.46KiB
No change  Initial CSS 2.22KiB 2.22KiB
Change  Cache Invalidation 0% 13.59%
No change  Chunks 9 9
No change  Assets 11 11
Change  Modules 227(-0.87%) 229
No change  Duplicate Modules 11 11
No change  Duplicate Code 27.28% 27.28%
No change  Packages 10 10
No change  Duplicate Packages 0 0
Bundle size by type  no changes
                 Current
#9577
     Baseline
#9570
No change  JS 495.91KiB 495.91KiB
No change  Other 401.92KiB 401.92KiB
No change  CSS 2.22KiB 2.22KiB

Bundle analysis reportBranch feat/sync-then-chunk-loadProject dashboard


Generated by RelativeCIDocumentationReport issue

@relativeci
Copy link
Copy Markdown

relativeci Bot commented May 11, 2026

React Example

#8005 Bundle Size — 235.77KiB (0%).

5abf278(current) vs d588d03 main#7998(baseline)

Bundle metrics  no changes
                 Current
#8005
     Baseline
#7998
No change  Initial JS 0B 0B
No change  Initial CSS 0B 0B
No change  Cache Invalidation 0% 0%
No change  Chunks 0 0
No change  Assets 4 4
No change  Modules 197 197
No change  Duplicate Modules 80 80
No change  Duplicate Code 44.85% 44.85%
No change  Packages 2 2
No change  Duplicate Packages 0 0
Bundle size by type  no changes
                 Current
#8005
     Baseline
#7998
No change  IMG 145.76KiB 145.76KiB
No change  Other 90.01KiB 90.01KiB

Bundle analysis reportBranch feat/sync-then-chunk-loadProject dashboard


Generated by RelativeCIDocumentationReport issue

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants