Repository navigation
Server For keeps its rows across live-hole sweeps (#4056) - #4057
Conversation
A markup hole that re-emits re-evaluated every row of a list on every write. During a sweep, a row whose item is the same reference at the same index now reuses the row rendered on the previous sweep. Plain SSR and a server component's first render never read or fill the cache. The chat example renders its finished messages through <For> with rows that keep their identity, so a streaming reply no longer re-renders the whole history on every write. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VywmD8RauUVvMzG8pYPUX8
🦋 Changeset detectedLatest commit: 93f17ce 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 |
Size (brotli, eager entry chunk)
|
Coverage Report for CI Build 38599202096Coverage remained the same at 77.515%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
Merging this PR will improve performance by 23.4%
|
| Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|
| ⚡ | memo + sync render effect only (reference) |
32.4 ms | 26.2 ms | +23.4% |
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-ico60t (93f17ce) with next (f286a40)
|
The CodSpeed Performance Analysis failure isn't caused by this PR. The two benchmarks it flags are both in Generated by Claude Code |
#4056) A live source usually hands back a fresh snapshot on every change, so a reference check alone never hits. A row whose item at the same index is shallow-equal to last sweep's now reuses its row, which removes the identity-keeping helper from the chat example. Chat's Regenerate button moves to CSS so a row depends only on its own message. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VywmD8RauUVvMzG8pYPUX8
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VywmD8RauUVvMzG8pYPUX8
Requested by Ryan · project thread
Summary
Before: a
<For>inside a markup hole that re-emits re-ranmapFnfor every row on every sweep. That covers live server components and standing sources. In the chat example, each streaming write re-rendered the markdown and highlighting of the whole conversation, so server CPU per reply grew by about 1.2 s with each earlier reply.After: during a sweep, a row whose item at the same index is unchanged reuses the row it rendered on the previous sweep. "Unchanged" means the same reference or a shallow-equal object, since a live source usually hands back a fresh snapshot. Chat's server CPU per reply stays flat as the thread grows, and the app needs no extra machinery.
How:
mapArray(packages/solid/src/server/signals.ts): keeps{ items, rows }from the last pass, but only whilesharedConfig.context.liveHoles.sweepingis true. A row is reused whensameItem(prev.items[i], items[i])holds, meaning===or a shallow compare of own keys.$keystays morph-only identity for the client.<For>. The Retry/Regenerate form now rides on every finished reply, and CSS shows it only on the last message (.messages > li:not(:last-child) form.again), so a row depends only on its own message. The server already refuses to regenerate anything but the last reply.Semantics: a row is a function of its item and index. An item mutated in place keeps its old row, and a row that reads outside data keeps the value it rendered with.
Repeatis deliberately left out. Its rows depend only on their index, so most real rows read outside data and reuse would go stale.How did you test this change?
packages/web/test/server/live-hole-for-rows-4056.spec.tsx:mapFnrunsA,B,BBinstead ofA,A,A,B,A,BB,A,BB, both for the same item objects and for fresh shallow-equal snapshots.solid-js: 860/860 pass. web client 1392/1392 and hydrate 501 pass. web server 1671 pass with 1 failure: the adapter-request "rebuild failure" test. It fails only on Node 22, which the local container runs, and passes on Node 24, which is what.nvmrcand CI use.vite buildpluscheck-bundleare clean. In a browser after six replies, only the last message's Regenerate form is visible.*The first reply includes server warm-up. Markdown calls show the row reuse directly: they stay at about one per write however long the thread gets.
The wire was flat in both runs, at about 27 KB brotli per reply. Client time is essentially unchanged: the wire still carries whole-hole HTML. Per-row ops on the wire are the follow-up in #4056.
🤖 Generated with Claude Code
https://claude.ai/code/session_01VywmD8RauUVvMzG8pYPUX8