Repository navigation
fix(solid): lazy() drops a loader promise made under hydration tracing (#4013) - #4019
Conversation
#4013) Hydration traces an adopted compute with the global Promise swapped for a never-settling MockPromise. A lazy()/lazyModule() loader that builds its promise from the global Promise (Vite's __vitePreload) returned that stub when first called inside the trace, and the module cache kept it, so .preload() and later renders never settled. subFetch now tracks its trace depth, and load() drops a promise that is an instance of the swapped-in Promise while tracing, so the next call loads for real. A real promise (e.g. a native import()) is still cached. CSR bundles fold the guard away. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NcUAu8BQ6Xsf8XRBhr9V1w
Size (brotli, eager entry chunk)
Bundled with Rolldown (what Vite ships), brotli q11, decimal KB. A scenario fails only when it is over its brotli cap and its minified size is more than 20 B over the minified recorded with the cap; over the cap within that allowance is brotli layout noise and passes with a warning. Caps and their recorded minified in |
Coverage Report for CI Build 38440116839Coverage increased (+0.09%) to 77.515%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
Drop the `instanceof Promise` half of the guard: lazy()/lazyModule() now skip caching any load started while subFetch is tracing, not only the MockPromise stub. A real import() started under the trace still lands through its then() (setting the component/module), and the next call re-runs the loader, which the module registry dedupes. Cuts the cost in hydrating bundles from +56 B to +34 B minified; CSR bundles are unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NcUAu8BQ6Xsf8XRBhr9V1w
Generated by Claude Code |
🦋 Changeset detectedLatest commit: ee4b6e5 The changes in this PR will be included in the next version bump. This PR includes changesets to release 13 packages
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 |
Merging this PR will not alter performance
|
Generated by Claude Code |
…zy() guard Measured against next @ c9027a9: compiled hydrating 31,610 -> 31,622 B (2 B over), hydrating + every store primitive family 29,440 -> 29,506 B (6 B over). Caps set at measured + 10 B rounded up to 0.01 KB. Accepted by the maintainer 2026-10-13. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NcUAu8BQ6Xsf8XRBhr9V1w
Requested by Ryan · project thread
Summary
Fixes #4013.
Before: when hydration adopts a server value, it still runs the client compute once as a trace, with the global
Promiseswapped for the never-settlingMockPromise. Alazy()loader that builds its promise from the globalPromise(Vite's__vitePreloaddoes) got the stub back, andlazy()cached it for good. Every later.preload()or render then waited forever.After:
lazy()andlazyModule()don't cache a loader promise made while a trace is running, so the next real call loads the module.How:
subFetchkeeps an internaltracingdepth counter.load()inlazy()andlazyModule()skips caching whiletracingis non-zero. A realimport()started during the trace still lands through itsthen(), and the module registry dedupes the next call.lazyModule()had the same caching pattern, so it gets the same guard.Public API changes
None.
Size
Measured against
next@ c9027a9:Both caps carry Size-Exception notes in
scripts/size/scenarios.js, accepted by the maintainer on 2026-10-13. Floor caps are unchanged.I also tried a smaller variant that compares the global
Promiseto a captured native one. It grew the compiled CSR scenario by 53 B, so this PR keeps the counter.How did you test this change?
packages/solid/test/client-hydration.spec.ts:lazy().preload()andlazyModule()under an adoption trace. Both time out on unmodifiednext.packages/web/test/hydration/lazy-preload-trace-4013.spec.tsx: realhydrate()against a serialized memo that awaitsLazy.preload(). It times out before the fix and passes after.solid-js: 858 passed.@solidjs/webclient: 1389 passed.@solidjs/webhydrate: 494 passed, plus 16 expected failures.@solidjs/webserver: 1651 passed, with 1 failure that also fails on unmodifiednext(server-functions-adapter-request.spec.tsx, rebuild failure case).🤖 Generated with Claude Code
https://claude.ai/code/session_01NcUAu8BQ6Xsf8XRBhr9V1w
Generated by Claude Code