You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Server components: a markup hole that re-emits re-renders every row of its list on every sweep #4056
A markup hole that emits more than once re-renders its whole value on every sweep. This applies to live addresses, plain responses reading a standing source, and the document face's sc:live ops. Server <For> / mapArray re-runs mapFn for every row on every pass, which is deliberate today (packages/solid/src/server/signals.ts, "never re-runs in isolation"). The sweep then ships the hole's full HTML unless it is byte-identical (packages/web/src/server.ts, sink.hole).
The result is that a list inside a streamed hole costs O(list) server render per write. Over a stream, that is O(list × writes).
Measured on examples/chat
Setup: simulated model, 12 KB replies, about 258 writes per reply, one tab, client CPU at 4x. The local instrumentation is a small patch, not committed.
prior replies
server CPU per reply
of which markdown
wire (brotli q4)
client script / style+layout
0
1.5 s
0.7 s
43 KB
266 / 403 ms
1
2.5 s
1.8 s
27 KB
196 / 328 ms
3
5.1 s
4.2 s
28 KB
262 / 416 ms
5
7.4 s
6.4 s
27 KB
339 / 348 ms
Each finished reply adds about 1.2 s of server CPU to every later reply. The finished-messages hole re-renders all of the history on every write, including markdown and highlighting, and then discards the result as identical. Each open tab pays this separately.
The wire stays flat because the streaming brotli window absorbs re-sent prefixes. That only holds within one response and with a large window. With gzip's 32 KB window, a 60 KB hole re-sent per write measured 11.3 MB, versus 49 KB for an append-only stream.
Direction
Make server For behave like the client's in renders that can re-emit a hole:
Keep rows by key across passes and reuse the last HTML of unchanged rows. Only new and changed rows render.
Data needs stable identity across snapshots. For example, reconcile each snapshot into a store or projection, as the client does. Today watchThread clones every message on each write.
The wire can then carry per-row ops (insert, replace, remove, move) instead of the whole hole. That is the "diff payload", with no string diffing needed.
One-shot document SSR keeps today's path.
A growing single row (the message being written) still re-renders in full. Splitting it into keyed blocks would reuse the same mechanism.
Closing. #4057 fixed the cost this issue measured: a live-hole sweep's server For re-rendered every row, and server CPU per chat reply now stays flat as history grows instead of climbing.
What's left is client cost, and it's tracked separately:
The whole-frame walk after every hole apply: its own PR, in progress.
Row ops (sending only the changed rows): on chat the wire stays flat at about 27 KB br per reply under streaming brotli, so bytes aren't the issue there. But the client still parses and morphs every unchanged row in a re-sent hole. On a page that is one big hole ticking (the server DBmon example), that parse may be what costs most. I'll measure it there once the walk fix lands, and open a new issue for row ops if it shows up.
An append patch for chat's growing message: later.
Activity
Reuse a swept row when its item is shallow-equal, drop the chat keeper (
Merge pull request #4057 from solidjs/claude/project-thread-ico60t
ryansolid commentedon Oct 14, 2026
Closing. #4057 fixed the cost this issue measured: a live-hole sweep's server
Forre-rendered every row, and server CPU per chat reply now stays flat as history grows instead of climbing.What's left is client cost, and it's tracked separately:
Generated by Claude Code
ryansolid commentedon Oct 14, 2026
The DBmon check came back: row diffs are the lever there (the client morph of unchanged rows, not bytes). Tracked in #4064.
Generated by Claude Code