Repository navigation
fix(solid): judge SSR waterfall waits by when their work started - #4108
Conversation
…4105) On a cold module cache, nested lazy() route levels resolve one level per <Loading> render pass even when the router kicked every matched level's import in the first pass, so SSR_BOUNDARY_WATERFALL reported parallel chunk loads as sequential data waits and pointed at queries. lazy()/lazyModule() now mark their import and asset-lookup promises (dev only), and the boundary leaves passes that waited only on those out of the waterfall count. The record's `passes` is unchanged; the finding gains `codePasses` when any were left out. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018LY7F9czoRqdMEvk2QwPr1
🦋 Changeset detectedLatest commit: b75d93b The changes in this PR will be included in the next version bump. This PR includes changesets to release 14 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 38852941060Coverage increased (+0.02%) to 77.554%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
Merging this PR will not alter performance
|
Generated by Claude Code |
Replaces the lazy()-only exclusion with the client's origin rule. A <Loading> boundary meets a nested route's levels one render pass at a time, but the router starts every matched level's lazy() import and preload together; SSR_BOUNDARY_WATERFALL counted each pass as a sequential wait. A wait now counts only if something it waited on started at or after the previous wait landed (`data.parallelWaits` names the ones left out). The start stamps move from the attribution engine onto the core's `OBSERVE.attribution` (`markFlight`, `flightOrigin`), so a router or cache can stamp what it hands out without loading the engine, and the server can read them. `attribution.markFlight` writes to the same map. lazy() and lazyModule() stamp their import (and lazy()'s client read wrapper); the server's async memo carries a stamp from its source promise onto the deferred the boundary waits on; lazy asset lookups and their gates carry their start. Prod artifacts are unchanged; the observe tier pays ~56 B br. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018LY7F9czoRqdMEvk2QwPr1
# Conflicts: # scripts/size/scenarios.js
query() hands each reader a fresh `.then` wrapper over the fetch. A route's preload starts the fetch; the page reads it a render pass later behind its lazy chunk, and the waterfall checks (ASYNC_WATERFALL on the client, SSR_BOUNDARY_WATERFALL on the server) read that wrapper as work that only started then. Stamp the fetch when it starts and each wrapper with the fetch's start through `OBSERVE.attribution.markFlight` (solidjs/solid#4108). Observe and dev only: `OBSERVE` folds out of shipped bundles, and a solid-js without the stamp functions is left alone. Claude-Session: https://claude.ai/code/session_018LY7F9czoRqdMEvk2QwPr1 Co-authored-by: Claude <noreply@anthropic.com>
Resolves the size-cap conflict with #4108's observe-tier raises: the list scenarios' caps are re-based on the merged tree (measured + 10 B), the observe-tier ones on top of #4108's new values, and the two router pages keep the caps CI's own bytes set; ledger lines say which. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Summary
Fixes #4105.
Before: on the first request to a cold server (a fresh dev server, or the first render after an edit invalidates a route module), a nested file route warned
[SSR_BOUNDARY_WATERFALL] … 3 sequential async waitsand pointed at queries, even with every query preloaded and behind its own<Loading>.After: the server check judges each wait by when its work started, using the same origin rule as the client's
ASYNC_WATERFALL. Preloaded route chunks and preloaded data no longer read as a chain. Route chunks nobody preloaded still do, and so does a real data chain, whose message names the waits it left out (… 4 sequential async waits (not counting 1 on work already started: a preload or lazy() import)).The router was already doing the right thing:
createRouteContextcallscomponent.preload()and runspreloadfor every matched level in one pass. The boundary still meets each level a render pass later, because a child is only reached once its parent has rendered. The old check counted passes, so it read work that was already running as sequential.How:
OBSERVE.attributionasmarkFlight(flight, startedAt?)andflightOrigin(flight). A router or cache can stamp what it hands out without loading the engine (the router already callsOBSERVE && OBSERVE.attribution.withOriginthe same way), and the server can read the stamps.attribution.markFlightnow writes to the same map, so the client engine's behaviour is unchanged. No new exports.data.parallelWaitswhen any waits were left out. The boundary record'spassesis unchanged.lazy()andlazyModule()stamp their import when it starts, on both client and server. The client also stampslazy()'s read wrapper. The server's async memo carries its source promise's stamp onto the deferred the boundary waits on. Lazy asset lookups and their gates carry their start.IS_OBSERVEand server ones onIS_DEV. The prod artifacts don't change (dist/solid.js,dist/server.jsand the signals prod build carry none of it).SSR_BOUNDARY_WATERFALLsection of the diagnostics skill describes the rule and how a cache should stamp.A companion router PR stamps
query()'s fetches and the wrappers it hands out, so preloaded route data is covered too. A cache that doesn't stamp falls back to today's behaviour.Size: the observe tier only. Against
nextwith #4107 merged in,app: CSR, observe tiergoes 14,498 → 14,588 B br (+90 B; +228 B minified) and+ attribution engine enabled29,108 → 29,180 B (+72 B; +210 B minified). Both caps are raised with ledger notes. Every prod scenario is unchanged.How did you test this change?
packages/web/test/server/lazy-waterfall-diagnostic.spec.tsx, with three cold nested lazy levels on an async (dev-style) manifest:parallelWaits: 1.query()does, gives no finding when stamped and a countedinfowhen not.next.packages/signals: a new test checks that a mark made throughOBSERVE.attribution.markFlight(no engine call) breaks the client chain the same way. The signals suite passes: 5,315 tests.packages/solid: 868 passed.packages/webclient: 1,403 passed.packages/webserver: 1,735 passed, with only the 2 known environmental failures (the Node 22 adapter test and [2.0 rc.15 / next] SSR never evaluates arefon an element that has a spread, so a directive factory's setup misaligns hydration ids #4081's compiler binary).node size.mjslocally,check-floor-caps.mjs origin/next(no floor raised), prettier, andtscfor solid.🤖 Generated with Claude Code
https://claude.ai/code/session_018LY7F9czoRqdMEvk2QwPr1
Generated by Claude Code