Skip to content

feat(solid): onHydrationEnd is public as @experimental (#4000) - #4024

Merged
ryansolid merged 2 commits into
nextfrom
claude/project-thread-w8qjvi-4000
Oct 13, 2026
Merged

ryansolid merged 2 commits into
nextfrom
claude/project-thread-w8qjvi-4000

Conversation

@ryansolid

Copy link
Copy Markdown
Member

Requested by Ryan · project thread

Summary

Closes #4000.

Before: onHydrationEnd was a runtime export of solid-js but @internal, so E2E tests had no documented way to tell when hydration finished and read _$HY.events or sharedConfig instead.

After: onHydrationEnd(callback) is public from solid-js, tagged @experimental, with docs scoped to test tooling and markers. sharedConfig and isHydrationInProgress stay internal.

How: the client export moves out of the @internal container-trace block in src/index.ts; the declaration in client/hydration.ts gets full JSDoc; the server stub moves to server/hydration.ts next to isHydrating. Docs: a new section in documentation/solid-2.0/12-ssr-http.md with a Playwright example, and one sentence in frames-rulings Ruling 3.

The documented caveats, each checked against the code:

  • It marks the end of the hydration phase, not success: a failed document-root module preload still runs the callback.
  • Register it after hydrate(). Registered before, it runs in the next microtask. The same happens when hydrate() parks because an async entry ran while the parser was still inside the shell (hydrate() run mid-parse leaves dead server nodes beside fresh client copies (async entry script) #3971 path); that case is documented and pinned in a test, with no runtime change.
  • With several hydration roots it is not page-wide completion (GH5).

Public API changes

  • New public export: onHydrationEnd(callback: () => void): void from solid-js (client and server types), @experimental. It was already a runtime export, so it adds no runtime bytes; client dist files are byte-identical.

Notes for review

  • A throwing callback drops the remaining callbacks and skips the deferred _$HY.done (read from the code, not tested). Wrapping each call costs a few bytes; not done here.
  • The server stub moved files for symmetry with isHydrating; it can stay in server/index.ts if byte-identical server dists matter more.

How did you test this change?

  • New packages/web/test/hydration/on-hydration-end-public.spec.tsx (6 tests, imports from solid-js): fires after hydrate, before-hydrate registration, failed document-root preload, non-document root fallback, the parked hydrate() run mid-parse leaves dead server nodes beside fresh client copies (async entry script) #3971 case.
  • internal-surface.spec.ts: onHydrationEnd now required in both declaration entries. published-declarations.spec.ts: the hydration probe imports it and checks its signature.
  • solid-js 858 passed; web hydrate 499 (+16 expected fail), default 1389; web server has 1 failure that also fails on unmodified next. pnpm typecheck 19/19; web test-types fails on next with the new spec (the export was missing from types) and passes here.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NcUAu8BQ6Xsf8XRBhr9V1w


Generated by Claude Code

`onHydrationEnd` was already a runtime export of both `solid-js` entries,
stripped from the declarations as `@internal`. It now leaves the
container-trace seam block and is declared next to `isHydrating` /
`isHydratable` on both entries, tagged `@experimental` like `setLinkClaim`
(#3921), so end-to-end tests have a documented end-of-hydration signal
instead of reading `_$HY`. No runtime bytes. `sharedConfig` and
`isHydrationInProgress` stay internal.

The JSDoc and RFC 12 say what it does not promise: register it after
`hydrate()` (before, or after a `hydrate()` parked mid-parse per #3971, it
runs in the next microtask); it ends with a failed document-root module
preload too; with several roots it can fire before a preload-deferred root
claims (GH5); it is for test tooling, not app branching.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NcUAu8BQ6Xsf8XRBhr9V1w
@ryansolid ryansolid self-assigned this Oct 13, 2026
@changeset-bot

changeset-bot Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 63044f7

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

This PR includes changesets to release 13 packages
Name Type
solid-js Patch
@solidjs/diagnostics Patch
@solidjs/element Patch
@solidjs/h Patch
@solidjs/html Patch
test-integration Patch
@solidjs/universal Patch
@solidjs/web Patch
board-example Patch
todos-server-example Patch
@solidjs/babel-plugin Patch
@solidjs/compiler Patch
@solidjs/signals 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 Oct 13, 2026 •

Copy link
Copy Markdown

Size (brotli, eager entry chunk)

scenario head vs base minified vs base minified vs recorded cap lazy chunks (not counted)
signals: core floor (createSignal/Memo/Effect/Root/flush) 7.44 KB 0 B 0 B +31 B 7.45 KB ✅
signals: + createStore 14.88 KB 0 B 0 B 0 B 14.89 KB ✅
signals: + isPending/latest 9.71 KB 0 B 0 B −72 B 9.73 KB ✅
app: render + one signal (the simple-app floor) 9.92 KB 0 B 0 B −39 B 9.94 KB ✅
app: hydrating (no stores) with Show/For/Loading/Errored/lazy 17.84 KB 0 B 0 B −166 B 17.93 KB ✅ lazy-page.js 0.04 KB
app: hydrating + every store primitive family 29.45 KB 0 B 0 B +28 B 29.50 KB ✅ lazy-page.js 0.04 KB
app: CSR with Show/For/Loading/Errored/lazy 13.01 KB 0 B 0 B 0 B 13.03 KB ✅ lazy-page.js 0.04 KB
app: CSR, observe tier (same app on the observe artifacts) 14.62 KB 0 B 0 B 0 B 14.59 KB ⚠️ over by 31 B, 20 B minified headroom lazy-page.js 0.04 KB
app: CSR, observe tier + attribution engine enabled 28.93 KB 0 B 0 B 0 B 28.96 KB ✅ lazy-page.js 0.04 KB
app: compiled floor (one template, one text hole, one delegated click) 10.13 KB 0 B 0 B −57 B 10.13 KB ⚠️ over by 1 B, 77 B minified headroom
app: compiled CSR (JSX todo app: spread/merge/omit, events, class/style, keyed For, Show, Loading + lazy, store) 25.64 KB 0 B 0 B +70 B 25.65 KB ✅ stats.js 0.18 KB
app: compiled hydrating (the same JSX todo app through hydrate(), compiled hydratable) 31.56 KB 0 B 0 B 0 B 31.58 KB ✅ stats.js 0.20 KB
frames: eager client consumer (frames client + transport, lazy codec) 11.43 KB 0 B 0 B 0 B 11.43 KB ✅
page: base server components (hydrating + dynamic + frames + sf reference) 34.08 KB 0 B 0 B −1715 B 34.33 KB ✅ assets.js 0.78 KB, bind.js 1.83 KB, decode.js 6.23 KB, lazy-page.js 0.04 KB, regions.js 0.80 KB, trace.js 8.99 KB, wire.js 0.93 KB
page: live server components (base + live/GET + action + isPending/latest) 37.86 KB 0 B 0 B −1803 B 38.15 KB ✅ assets.js 0.78 KB, bind.js 1.83 KB, decode.js 6.23 KB, lazy-page.js 0.04 KB, regions.js 0.80 KB, trace.js 9.00 KB, wire.js 0.93 KB
page: compiled base server components (the base page as JSX: templates with class/style/attributes/events, For/Show; no spread) 34.97 KB 0 B 0 B −2793 B 35.66 KB ✅ assets.js 0.78 KB, bind.js 1.83 KB, decode.js 6.23 KB, regions.js 0.80 KB, sc-comments.js 0.20 KB, trace.js 8.97 KB, wire.js 0.93 KB
page: compiled live server components (the compiled base page + live/GET + action + isPending/latest) 40.51 KB 0 B 0 B −2835 B 41.21 KB ✅ eager (counted): web.js 21.47 KB; assets.js 0.78 KB, bind.js 1.83 KB, decode.js 6.23 KB, regions.js 0.79 KB, sc-comments.js 0.19 KB, trace.js 8.95 KB, wire.js 0.93 KB
page: base + router (base page + @solidjs/router: createRouter, two routes, preload, useNavigate) 41.37 KB 0 B 0 B −1702 B 41.79 KB ✅ assets.js 0.78 KB, bind.js 1.85 KB, decode.js 6.23 KB, lazy-page.js 0.04 KB, regions.js 0.80 KB, server.js 1.02 KB, serverForms.js 3.58 KB, trace.js 8.97 KB, wire.js 0.94 KB
page: live + router (live page + @solidjs/router: createRouter, two routes, preload, useNavigate) 47.19 KB 0 B 0 B −1751 B 47.49 KB ✅ eager (counted): client.js 27.69 KB; assets.js 0.78 KB, bind.js 1.85 KB, decode.js 6.23 KB, lazy-page.js 0.04 KB, regions.js 0.81 KB, server.js 1.02 KB, serverForms.js 3.32 KB, trace.js 8.95 KB, wire.js 0.94 KB
server: floor (getRequestEvent + isServer) 1.33 KB 0 B 0 B 0 B 1.34 KB ✅
server: renderToString (the server-render floor) 20.47 KB 0 B 0 B 0 B 20.48 KB ✅

⚠️ Over the brotli cap within the minified allowance (passes)

  • app: CSR, observe tier (same app on the observe artifacts): over brotli cap by 31 B; minified 41,460 B vs 41,460 B recorded with the cap (+0 B) — 20 B of the 20 B minified allowance left; +0 B minified over this PR's base
  • app: compiled floor (one template, one text hole, one delegated click): over brotli cap by 1 B; minified 28,386 B vs 28,443 B recorded with the cap (−57 B) — 77 B of the 20 B minified allowance left; +0 B minified over this PR's base

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 scripts/size/scenarios.js; the floor and page caps in floor-caps.json are frozen (lower only, or Size-Exception: in the PR body). npm run ratchet lowers caps per RC; it never raises one (scripts/size/README.md).

Copy link
Copy Markdown
Member Author

The red Size / check here is "app: compiled hydrating", over its brotli cap by 31 B with a 0 B delta against the base. It isn't from this PR: every scenario is unchanged, and the same scenario fails on next itself. A cap fix for next is being handled separately.

— Claude via Claude Code


Generated by Claude Code

@coveralls

coveralls commented Oct 13, 2026 •

Copy link
Copy Markdown

Coverage Report for CI Build 38433181592

Coverage remained the same at 77.429%

Details

  • Coverage remained the same as the base build.
  • Patch coverage: No coverable lines changed in this PR.
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 1321
Covered Lines: 1079
Line Coverage: 81.68%
Relevant Branches: 1036
Covered Branches: 746
Branch Coverage: 72.01%
Branches in Coverage %: Yes
Coverage Strength: 27.08 hits per line

💛 - Coveralls

@codspeed

codspeed Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

Merging this PR will improve performance by 10.2%

⚠️ 2 benchmarks spent significant time in system calls

System calls cannot be consistently instrumented, so they are not included in the measure, which understates the real cost. Please switch to the Walltime instrument to accurately measure system calls.

Measurement and system calls

⚠️ 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

⚡ 2 improved benchmarks
✅ 193 untouched benchmarks

Performance Changes

Benchmark BASE HEAD Efficiency
⚡ createStore setter: delete + set one root key (#3044 overlay) 2.1 ms 1.8 ms +13.31%
⚡ memo + sync render effect only (reference) 28.6 ms 26.7 ms +7.17%

Tip

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


Comparing claude/project-thread-w8qjvi-4000 (63044f7) with next (be845cb)

Open in CodSpeed

Copy link
Copy Markdown
Member Author

CodSpeed flags "createStore setter: delete + set one root key" (−12%). This PR changes no runtime code: client dist files are byte-identical to next, and the benchmark is in @solidjs/signals, which this PR doesn't touch. The same benchmark is flagged on the compiler-only #4021, and the rerun on #4020 came back clean, so this looks like CodSpeed environment variance (its report notes the compared runs used different runtime environments). I'm re-running the workflow once to confirm.

— Claude via Claude Code


Generated by Claude Code

Copy link
Copy Markdown
Member Author

CodSpeed reports memo + sync render effect only (reference) 11.9% slower after the merge of next, but this PR's own change isn't the cause. That benchmark is pure signals, and this PR only touches the hydration-end hook in solid-js/@solidjs/web. CodSpeed also flags that the comparison ran on different runtime environments, and the same benchmark read 8.9% faster on #4027 against the same base. Size and tests are green. I've re-run the benchmarks once.

— Claude via Claude Code


Generated by Claude Code

@ryansolid
ryansolid marked this pull request as ready for review October 13, 2026 17:02
@ryansolid
ryansolid merged commit 25935fe into next Oct 13, 2026
9 checks passed
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.

3 participants