Skip to content

fix(web): give pending fragments one task before the shell flushes - #4147

Merged
ryansolid merged 1 commit into
nextfrom
claude/project-thread-u5n0br
Oct 19, 2026
Merged

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

Conversation

@ryansolid

Copy link
Copy Markdown
Member

Requested by Ryan · project thread

Summary

Before: renderToStream gave pending fragments only a few microtask turns (MIN_DRAIN_TURNS) before it flushed the shell. If an async read took more than about three microtasks, its Loading fallback shipped along with a <template> and a reveal script, even when the data was effectively instant. That includes a "use server" query over in-memory data going through the router. On a store listing and product page this showed up as a layout shift when the reveal pushed the footer down a screen (CLS 0.75 in most Lighthouse runs).

After: once the microtask drain settles and fragments are still pending, the shell waits one more task (setImmediate, or the existing deferFlush fallback on hosts without it) before flushing. Data ready by then renders inline with no fallback, template or reveal. Renders with nothing pending flush exactly as before. Genuinely pending I/O pays one task of first-byte latency. 1.x waited a task before every shell flush.

How: one graced flag per flush attempt in scheduleFlush. When the drain would otherwise call the flush and registry.size is non-zero, the attempt re-queues itself once on deferFlush and drains again.

How did you test this change?

  • New test/server/shell-task-grace.spec.tsx: data ready after a 20-step microtask chain, and data ready on setImmediate, both render inline in the first chunk. Data gated until after the first chunk still streams behind its fallback. The two inline cases fail on next and pass here.
  • frame-plain-bound.spec.tsx: the live-response test pushed its first value before the shell had flushed and expected it to stream. It now waits for the shell chunk first, like the other tests in that file.
  • pnpm run build, then pnpm turbo run test test-types typecheck --continue: everything passes except server-functions-adapter-request "rebuild failure", which also fails on next under Node 22.22 locally. The web hydrate suite (511 tests) and client suite (1383) pass.
  • An SSR SPA store app (48-product listing plus product pages, built against this branch's @solidjs/web): on next, all three pages ship Loading... plus a <template>. With this change they render inline (0 templates, about 2 kB smaller each).

Generated by Claude Code

renderToStream granted pending fragments only a handful of microtask turns
before flushing the shell, so a read whose retry chain is longer than that
(a server function over in-memory data, a cache answering on setImmediate)
shipped its Loading fallback, a <template> and a reveal, causing layout shift
even though the data was ready. The shell now waits one task (setImmediate)
after the drain settles while fragments are pending. Renders with nothing
pending are unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011c7QLxhYyzAcek32Sun3pK
@changeset-bot

changeset-bot Bot commented Oct 19, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 46db1cf

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

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

@ryansolid ryansolid self-assigned this Oct 19, 2026
@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.25 KB 0 B 0 B +8 B 7.25 KB ✅
signals: + createStore 14.78 KB 0 B 0 B +8 B 14.82 KB ✅
signals: + isPending/latest 9.96 KB 0 B 0 B 0 B 9.98 KB ✅
signals: + until/refresh (truth readers, no optimism) 7.86 KB 0 B 0 B +8 B 7.86 KB ✅
app: render + one signal (the simple-app floor) 9.56 KB 0 B 0 B −2 B 9.56 KB ⚠️ over by 3 B, 22 B minified headroom
app: hydrating (no stores) with Show/For/Loading/Errored/lazy 18.19 KB 0 B 0 B −2 B 18.19 KB ✅ lazy-page.js 0.04 KB
app: hydrating + every store primitive family 30.17 KB 0 B 0 B −6 B 30.14 KB ⚠️ over by 28 B, 26 B minified headroom lazy-page.js 0.04 KB
app: CSR with Show/For/Loading/Errored/lazy 13.13 KB 0 B 0 B −2 B 13.14 KB ✅ lazy-page.js 0.04 KB
app: CSR, observe tier (same app on the observe artifacts) 14.88 KB 0 B 0 B −2 B 14.91 KB ✅ lazy-page.js 0.04 KB
app: CSR, observe tier + attribution engine enabled 29.44 KB 0 B 0 B −2 B 29.48 KB ✅ lazy-page.js 0.04 KB
app: compiled floor (one template, one text hole, one delegated click) 9.76 KB 0 B 0 B 0 B 9.77 KB ✅
app: compiled CSR (JSX todo app: spread/merge/omit, events, class/style, keyed For, Show, Loading + lazy, store) 25.98 KB 0 B 0 B −2 B 26.03 KB ✅ stats.js 0.18 KB
app: compiled hydrating (the same JSX todo app through hydrate(), compiled hydratable) 31.98 KB 0 B 0 B +1 B 31.98 KB ✅ stats.js 0.20 KB
frames: eager client consumer (frames client + transport, lazy codec) 12.98 KB 0 B 0 B 0 B 12.99 KB ✅
page: base server components (hydrating + dynamic + frames + sf reference) 34.27 KB 0 B 0 B 0 B 34.28 KB ✅ assets.js 0.78 KB, decode.js 6.25 KB, lazy-page.js 0.04 KB, regions.js 1.07 KB, trace.js 9.03 KB, wire.js 1.33 KB
page: live server components (base + live/GET + action + isPending/latest) 38.56 KB 0 B 0 B 0 B 38.57 KB ✅ assets.js 0.78 KB, decode.js 6.25 KB, lazy-page.js 0.04 KB, regions.js 1.06 KB, trace.js 9.00 KB, wire.js 1.33 KB
page: compiled base server components (the base page as JSX: templates with class/style/attributes/events, For/Show; no spread) 36.41 KB 0 B 0 B 0 B 36.42 KB ✅ assets.js 0.78 KB, decode.js 6.25 KB, regions.js 1.06 KB, sc-comments.js 0.20 KB, trace.js 9.04 KB, wire.js 1.33 KB
page: compiled live server components (the compiled base page + live/GET + action + isPending/latest) 42.38 KB 0 B 0 B 0 B 42.39 KB ✅ eager (counted): web.js 21.79 KB; assets.js 0.78 KB, decode.js 6.25 KB, regions.js 1.06 KB, sc-comments.js 0.19 KB, trace.js 9.07 KB, wire.js 1.33 KB
page: base + router (base page + @solidjs/router: createRouter, two routes, preload, useNavigate) 41.80 KB 0 B 0 B 0 B 41.81 KB ✅ assets.js 0.78 KB, decode.js 6.25 KB, lazy-page.js 0.04 KB, regions.js 1.06 KB, server.js 1.02 KB, serverForms.js 3.59 KB, trace.js 9.10 KB, wire.js 1.33 KB
page: live + router (live page + @solidjs/router: createRouter, two routes, preload, useNavigate) 47.95 KB 0 B 0 B 0 B 47.97 KB ✅ eager (counted): client.js 26.92 KB; assets.js 0.78 KB, decode.js 6.25 KB, lazy-page.js 0.04 KB, regions.js 1.07 KB, server.js 1.02 KB, serverForms.js 3.30 KB, trace.js 9.07 KB, wire.js 1.34 KB
server: floor (getRequestEvent + isServer) 1.33 KB 0 B 0 B 0 B 1.34 KB ✅
server: renderToString (the server-render floor) 20.62 KB 0 B 0 B −23 B 20.59 KB ⚠️ over by 34 B, 43 B minified headroom

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

  • app: render + one signal (the simple-app floor): over brotli cap by 3 B; minified 26,662 B vs 26,664 B recorded with the cap (−2 B) — 22 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 28 B; minified 94,638 B vs 94,644 B recorded with the cap (−6 B) — 26 B of the 20 B minified allowance left; +0 B minified over this PR's base
  • server: renderToString (the server-render floor): over brotli cap by 34 B; minified 72,131 B vs 72,154 B recorded with the cap (−23 B) — 43 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 39205738161

Coverage remained the same at 77.555%

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: 1347
Covered Lines: 1103
Line Coverage: 81.89%
Relevant Branches: 1050
Covered Branches: 756
Branch Coverage: 72.0%
Branches in Coverage %: Yes
Coverage Strength: 27.55 hits per line

💛 - Coveralls

@codspeed

codspeed Bot commented Oct 19, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

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

✅ 196 untouched benchmarks
⏩ 5 skipped benchmarks1


Comparing claude/project-thread-u5n0br (46db1cf) with next (a9f7e2d)

Open in CodSpeed

Footnotes

  1. 5 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

@ryansolid
ryansolid marked this pull request as ready for review October 19, 2026 06:04
@ryansolid
ryansolid merged commit 096d4da into next Oct 19, 2026
8 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