perf(dashboard): hold rail skeletons back so empty rails stop shifting the page

On every launch with sources the dashboard shifted by about 0.23 (the
"good" CLS threshold is 0.1). The rails render as soon as their own data
arrives, and each loading rail showed a 328 px skeleton immediately. On a
normal profile the live-favorites and recent-content sources resolve empty
15-20 ms later, so their skeletons flashed and collapsed and every rail
below jumped up by about 360 px. J1 never saw it: its layout-shift window
ends at the first card, which is painted just before the collapse.

Rail skeletons now wait out a 300 ms grace period (createRailSkeletonGrace)
and appear only for a rail still loading after it; the hero keeps its
immediate skeleton because it reserves the top of the page. Recorded over
three renderer reloads of a seeded profile, the dashboard's layout shift
drops from 0.219-0.234 to 0.0004, with the real rails painted at the same
time as before. Plan thread C5, journey J1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Opus 5.5 committed 2026-09-27 21:16:37 +02:00
1 parent 1d9a563d1a
commit b7a5c36d71
6 files changed
+132 -4

No files matched your search

@@ -542,6 +542,20 @@ mirrors the row/card geometry it precedes (see Channel List Item and Cover
Grids). The unified Favorites/Recent page gates this on `isLoading`, set only
while its item list is empty.
### Pages of independently loading blocks: delayed skeletons
The dashboard renders each rail as soon as its own data arrives, and several
rails resolve empty on a normal profile. A per-rail skeleton shown
immediately therefore flashed for a few tens of milliseconds and collapsed,
pulling every rail below it upwards (a layout shift of about 0.23 on each
launch with sources). Rail skeletons wait out a short grace period
(`DASHBOARD_RAIL_SKELETON_GRACE_MS`, 300 ms, in
`libs/workspace/dashboard/feature/src/lib/rails/dashboard-skeleton-grace.ts`)
and appear only for a rail that is still loading after it. The top block
(the dashboard hero) keeps its immediate skeleton: it reserves the space
above everything else, where a late insertion would push the whole page down.
Use the same rule for any page that stacks independently loading blocks.
### Reload with content on screen: non-destructive indicator
A reload of a list that is already rendered (the collection page's