Skip to content

fix(server): re-render live For rows that read async sources - #4069

Merged
ryansolid merged 1 commit into
nextfrom
claude/project-thread-ntysj0
Oct 14, 2026
Merged

ryansolid merged 1 commit into
nextfrom
claude/project-thread-ntysj0

Conversation

@ryansolid

Copy link
Copy Markdown
Member

Requested by Ryan · project thread

Summary

Follow-up to #4057, which lets a server For inside a live hole reuse a row across sweeps when its item at the same index is unchanged.

Before: reuse only looked at the item. A row that also read an async memo or projection outside its item, such as a "selected" mark driven by a selected id, kept the markup from the sweep that first rendered it, so the selection never moved. An item edited in place also compared equal to itself and kept its old row.

After: a row is reused only if it read no async source while rendering. Async sources are async memos, async projections, and memos derived from them. Items are compared against a deep copy taken when the row rendered, so an in-place edit renders the row again.

How (packages/solid/src/server/signals.ts): async-source reads are counted. A memo compute takes a snapshot of the counter, and a memo read checks a flag. A sweep reuses a row only when its render made no such reads.

Cost. Plain SSR pays one counter snapshot per memo compute and one flag check per memo read. The item copy and the row cache run only during live sweeps. The 5-run server bench lane showed only noise, so CodSpeed on this PR has the exact numbers. Size scenarios are unchanged: renderToString stays at 20,570 B, and the client bundles are untouched.

How did you test this change?

  • live-hole-for-rows-4056.spec.tsx gains cases for a row that reads an async memo or projection beside its item, and for an item edited in place. Each case now renders the row again on every sweep. Rows that read only their item are still reused.
  • On next @ 0109a56 after pnpm build: solid-js 860/860 pass. @solidjs/web DOM 1394/1394 and hydrate 506 pass. The web server suite has 1688 passing and 1 failure: the adapter-request "rebuild failure" test, which fails only on Node 22 in the cloud container and passes on Node 24, the version .nvmrc and CI use.
  • scripts/size: renderToString is unchanged at 20,570 B / 72,373 minified.

The fix was written in the "Diff payloads for live holes" thread, and this PR carries its patch unchanged.

🤖 Generated with Claude Code

https://claude.ai/code/session_01KRieFbP8sapvTBygyVm3mg


Generated by Claude Code

A sweep reused a row whenever its item looked unchanged, so a row that
also read an async memo or projection (a selected id) kept stale markup,
and an item edited in place compared equal to itself. Count reads of
async sources (async memos, async projections, memos derived from them)
and only reuse rows that made none; compare items against a deep copy
taken when the row rendered.

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

changeset-bot Bot commented Oct 14, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 7c36f2b

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

This PR includes changesets to release 14 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
server-dbmon-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

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.47 KB 0 B 0 B +19 B 7.48 KB ✅
signals: + createStore 14.90 KB 0 B 0 B +6 B 14.94 KB ✅
signals: + isPending/latest 9.77 KB 0 B 0 B +18 B 9.77 KB ⚠️ over by 3 B, 2 B minified headroom
app: render + one signal (the simple-app floor) 9.98 KB 0 B 0 B +18 B 9.98 KB ⚠️ over by 2 B, 2 B minified headroom
app: hydrating (no stores) with Show/For/Loading/Errored/lazy 18.09 KB 0 B 0 B −9 B 18.09 KB ✅ lazy-page.js 0.04 KB
app: hydrating + every store primitive family 29.65 KB 0 B 0 B −6 B 29.65 KB ⚠️ over by 4 B, 26 B minified headroom lazy-page.js 0.04 KB
app: CSR with Show/For/Loading/Errored/lazy 13.20 KB 0 B 0 B −69 B 13.20 KB ✅ lazy-page.js 0.04 KB
app: CSR, observe tier (same app on the observe artifacts) 14.76 KB 0 B 0 B −67 B 14.79 KB ✅ lazy-page.js 0.04 KB
app: CSR, observe tier + attribution engine enabled 29.40 KB 0 B 0 B −66 B 29.38 KB ⚠️ over by 18 B, 86 B minified headroom lazy-page.js 0.04 KB
app: compiled floor (one template, one text hole, one delegated click) 10.19 KB 0 B 0 B +18 B 10.17 KB ⚠️ over by 17 B, 2 B minified headroom
app: compiled CSR (JSX todo app: spread/merge/omit, events, class/style, keyed For, Show, Loading + lazy, store) 25.85 KB 0 B 0 B −67 B 25.92 KB ✅ stats.js 0.18 KB
app: compiled hydrating (the same JSX todo app through hydrate(), compiled hydratable) 31.78 KB 0 B 0 B −6 B 31.78 KB ⚠️ over by 1 B, 26 B minified headroom stats.js 0.20 KB
frames: eager client consumer (frames client + transport, lazy codec) 11.54 KB 0 B 0 B 0 B 11.54 KB ✅
page: base server components (hydrating + dynamic + frames + sf reference) 34.33 KB 0 B 0 B −932 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 9.02 KB, wire.js 0.93 KB
page: live server components (base + live/GET + action + isPending/latest) 38.15 KB 0 B 0 B −996 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) 35.18 KB 0 B 0 B −2022 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.98 KB, wire.js 0.93 KB
page: compiled live server components (the compiled base page + live/GET + action + isPending/latest) 40.84 KB 0 B 0 B −2010 B 41.21 KB ✅ eager (counted): web.js 21.67 KB; assets.js 0.78 KB, bind.js 1.84 KB, decode.js 6.23 KB, regions.js 0.79 KB, sc-comments.js 0.19 KB, trace.js 8.98 KB, wire.js 0.93 KB
page: base + router (base page + @solidjs/router: createRouter, two routes, preload, useNavigate) 41.73 KB 0 B 0 B −938 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 9.02 KB, wire.js 0.94 KB
page: live + router (live page + @solidjs/router: createRouter, two routes, preload, useNavigate) 47.50 KB 0 B 0 B −932 B 47.49 KB ⚠️ over by 7 B, 952 B minified headroom eager (counted): client.js 27.90 KB; assets.js 0.78 KB, bind.js 1.84 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.30 KB, trace.js 9.00 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.57 KB 0 B 0 B 0 B 20.58 KB ✅

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

  • signals: + isPending/latest: over brotli cap by 3 B; minified 27,465 B vs 27,447 B recorded with the cap (+18 B) — 2 B of the 20 B minified allowance left; +0 B minified over this PR's base
  • app: render + one signal (the simple-app floor): over brotli cap by 2 B; minified 27,963 B vs 27,945 B recorded with the cap (+18 B) — 2 B of the 20 B minified allowance left; +0 B minified over this PR's base
  • app: hydrating + every store primitive family: over brotli cap by 4 B; minified 93,708 B vs 93,714 B recorded with the cap (−6 B) — 26 B of the 20 B minified allowance left; +0 B minified over this PR's base
  • app: CSR, observe tier + attribution engine enabled: over brotli cap by 18 B; minified 88,264 B vs 88,330 B recorded with the cap (−66 B) — 86 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 17 B; minified 28,493 B vs 28,475 B recorded with the cap (+18 B) — 2 B of the 20 B minified allowance left; +0 B minified over this PR's base
  • app: compiled hydrating (the same JSX todo app through hydrate(), compiled hydratable): over brotli cap by 1 B; minified 101,332 B vs 101,338 B recorded with the cap (−6 B) — 26 B of the 20 B minified allowance left; +0 B minified over this PR's base
  • page: live + router (live page + @solidjs/router: createRouter, two routes, preload, useNavigate): over brotli cap by 7 B; minified 143,192 B vs 144,124 B recorded with the cap (−932 B) — 952 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).

@coveralls

Copy link
Copy Markdown

Coverage Report for CI Build 38633726246

Coverage remained the same at 77.515%

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: 1326
Covered Lines: 1084
Line Coverage: 81.75%
Relevant Branches: 1040
Covered Branches: 750
Branch Coverage: 72.12%
Branches in Coverage %: Yes
Coverage Strength: 27.43 hits per line

💛 - Coveralls

@ryansolid
ryansolid merged commit 7cb98df into next Oct 14, 2026
7 checks passed
@codspeed

codspeed Bot commented Oct 14, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

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

✅ 195 untouched benchmarks


Comparing claude/project-thread-ntysj0 (7c36f2b) with next (0109a56)1

Open in CodSpeed

Footnotes

  1. No successful run was found on next (7c36f2b) during the generation of this report, so 0109a56 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report. ↩

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