PR #10981: the workspace layout now streams an empty frame (the rail column, the top-bar strip, the content well) at first byte, and the real frame replaces it when the layout's reads resolve. These captures put the two frames side by side from ONE page load each, at desktop width, phone width and in dark mode, with each frame's rail and top bar measured.
23 Sept 2026 · branch perf/prospects-hard-load-shell at 59afbe6d84 · captured against propflow-stage (checked from the running server's /api/health) · smoke user · route /leasing/prospects
How the ghost was held on screen. On a warm dev server the layout's reads finish in well under a second, too fast to photograph. For these captures only, a LOCAL, UNCOMMITTED line at the top of WorkspaceResolved waited 8 s when a capture-only cookie was present. Nothing else was changed. Each variant warms the route first, sets the cookie, reloads, and shoots twice in the same tab: once when the ghost is attached, once after the real top bar mounts. The line was removed after capture, and the branch contains no delay.
Why this page is hand-built. scripts/ui-evidence.ts gates each section on rendered text and waits for domcontentloaded. The ghost renders no text by design, and a held stream delays that event until the real frame lands. So the tool can't photograph this state. Instead, the same pieces (the stage-only target guard, the smoke sign-in and the cookie staging) drive a short Playwright script. The "assertion" is geometry: getBoundingClientRect() of each frame's rail and top bar, and the element that owns the top-left corner pixel.
The rail column is 240 px wide and full height in both frames, and the top bar spans the full 1440 px at 56 px tall in both. The real frame fills in the navigation, the picker and the page; nothing that was already painted moves. The rail owns the top-left corner in both frames: the ghost's rail sits at Z.rail, above the top bar's z-30, as SideNav does. That is the fix for the previous round's corner-colour finding.
Measured:
| Frame ghost | Real frame | |
|---|---|---|
| Rail | left 0 · top 0 · 240×900 | left 0 · top 0 · 240×900 |
| Top bar | left 0 · top 0 · 1440×56 | left 0 · top 0 · 1440×56 |
| Top-left corner (4,4) | div.pf-frame-ghost-rail bg=rgb(255, 255, 255) | nav.pf-nav-drawer bg=rgb(255, 255, 255) |
| Painted after | 0.5 s | 10.3 s |
Below 768 px the real rail is an off-canvas drawer (measured at left −288, i.e. fully off screen) and reserves no column. The ghost hides its rail column at the same breakpoint, so the phone never paints a sliver that then disappears. The top bar strip is the full 390 px in both frames.
Measured:
| Frame ghost | Real frame | |
|---|---|---|
| Rail | not painted | left -288 · top 0 · 288×844 |
| Top bar | left 0 · top 0 · 390×56 | left 0 · top 0 · 390×56 |
| Top-left corner (4,4) | div.z-30 bg=rgb(250, 250, 250) | header.fixed.left-0 bg=rgb(250, 250, 250) |
| Painted after | 0.5 s | 10.1 s |
With the browser set to dark and no stored preference, the pre-hydration script picks dark before the ghost paints. The ghost and the real frame share the same surface tokens (FRAME_SURFACE), so the rail and corner paint the same dark colour in both frames, with no light flash.
Measured:
| Frame ghost | Real frame | |
|---|---|---|
| Rail | left 0 · top 0 · 240×900 | left 0 · top 0 · 240×900 |
| Top bar | left 0 · top 0 · 1440×56 | left 0 · top 0 · 1440×56 |
| Top-left corner (4,4) | div.pf-frame-ghost-rail bg=rgb(10, 10, 15) | nav.pf-nav-drawer bg=rgb(10, 10, 15) |
| Painted after | 0.5 s | 10.3 s |
The content well is empty while the ghost is up. The page's own loading.tsx skeleton sits under the resolved layout, so it can't render inside the layout's fallback; that is how the PR describes the design. The capture is from a dev server, so the Next.js dev badge shows bottom-left in every frame. The 8 s hold is artificial; the production timing claim in the PR (a 9.3 s cold-load wait before first paint) is not measured here.