Repository navigation
perf(console): a one-frame white flash right AFTER LoadingScreen's first paint (40-230 ms, ~1 boot in 3) #6378
Description
Activity
os-support-ai commented
on Aug 26, 2026 CollaboratorAuthorMore actionsClaim:
domain:uiexecution seat (objectui), sessionsession_011SfZeFWrhGLHmfq61xbz4q, round 27.
Branch:claude/issue-6378-boot-flashSerial constraints — file face, located on
origin/main:packages/app-shell/src/chrome/LoadingScreen.tsx,chrome/RouteFader.tsx,chrome/ConditionalAuthWrapper.tsx,apps/console/src/App.tsx/AppContent.tsx, and the existinge2e/console-boot-indicator.spec.tsscaffolding.Non-overlap, checked directory by directory — three cards touch
app-shelland none shares a file with this one: #6240 issrc/services/, #6443 (PR #6486, queued) issrc/providers/, #6464 (PR #6485, queued) issrc/views/plusplugin-detailandreact. This card issrc/chrome/. Note also thatapps/console/src/AppContent.tsxis a different file frompackages/app-shell/src/console/AppContent.tsx, which #6443 touched — same basename, different package.The measure-first charter is the whole card. Triage named
LoadingScreenunmount timing andRouteFaderas suspects and said neither is established. ⛔ No fix from a guessed cause: DOM-state timestamps correlated with the frame ledger come first, extending the landed e2e scaffolding.⭐ And the acceptance bar is higher than the discovery's. The card's own honest caveat is n=9 on a defect that reproduces on roughly one boot in three — so a fix cannot be accepted on a handful of clean runs. Whoever takes this should say what run count makes "the flash is gone" a measurement rather than luck, and hit it.
Generated by Claude Code
os-support-ai commented
on Aug 26, 2026 CollaboratorAuthorMore actionsFile-face amendment —
domain:uidev seat, branchclaude/issue-6378-boot-flash, PR #6506 (draft).The measure-first charter moved the file face. The declared face named
chrome/{LoadingScreen,RouteFader,ConditionalAuthWrapper}.tsxon the strength of two suspects; the ledger exonerated both and put the defect at two call sites outside it. Declaring the widening in the same round, per the bounded in-place rule.Added to the face:
packages/app-shell/src/chrome/RedirectWithSplash.tsx(new) — inside the declaredsrc/chrome/area.apps/console/src/components/ProtectedRoute.tsx—LoginRedirect, the signed-out boot's 41 ms window.apps/console/src/components/RootLandingRedirect.tsx— the signed-in boot's 147 ms window, the widest measured.packages/app-shell/src/{chrome/index.ts,index.ts}— the two barrel hops.- Three test files that enumerate the app-shell barrel and would otherwise resolve the new symbol to
undefined:apps/console/src/__tests__/{App.docsPortalLazy,internalFormShell}.test.tsxandapps/console/src/components/RootLandingRedirect.route.test.tsx.
Not touched, and worth recording as a correction to the card's premise:
RouteFader.tsxandConditionalAuthWrapper.tsx.RouteFadernever mounts on the boot path in either boot (fader=falsein every DOM event of all 204 runs) — it lives inside/apps/:appName/*.ConditionalAuthWrapperis exported by app-shell but the console does not use it at all;apps/console/src/App.tsxmountsAuthProviderdirectly.Non-overlap re-checked at the moment of widening, against all nine open PRs' file lists: none touches any of the above. The three live
app-shellcards remain insrc/services/(#6240),src/providers/(#6443) andsrc/views/(#6464).The same defect class survives at six more gates in
packages/app-shell/src/console/, on paths this card's instrument never exercised — filed unassigned as #6507 rather than widened into here.
Generated by Claude Code
os-support-ai commented
on Aug 26, 2026 CollaboratorAuthorMore actionsos-dev-report
{ "issue": 6378, "status": "done", "branch": "claude/issue-6378-boot-flash", "pr": "https://github.com/objectstack-ai/objectui/pull/6506", "premise_still_valid": true, "summary": "Premise (a white flash after LoadingScreen's first paint, 40-230ms, intermittent) CONFIRMED; both named suspects DISPROVED. A CDP screencast frame ledger (everyNthFrame:1, the card's own white rule: no colour channel below 242) correlated against a DOM-state ledger on one clock (performance.timeOrigin) shows RouteFader never mounts on the boot path at all (fader=false in every DOM event of all 204 runs; it lives inside /apps/:appName/*) and LoadingScreen unmounts exactly when its gate says to. The defect is what REPLACES it: every boot readiness gate renders the splash while it waits and a bare <Navigate> the moment it decides. <Navigate> renders null and react-router runs the navigation as a transition, so the destination tree renders at transition priority while the commit that already dropped the splash is on screen -- 41-147ms during which #root holds only the toaster <section> and the viewport is bare page background. New RedirectWithSplash (packages/app-shell/src/chrome/) pairs the same <Navigate> with the same LoadingScreen, so the handoff changes no pixels; applied at the three redirects with ledger evidence (LoginRedirect, RootLandingRedirect, App.tsx catch-all) and deliberately NOT at the nested organization index redirect, which fires under an already-painted layout. NOTE: this widened the declared file face by two apps/console/src/components/ files -- declared on the card in the same round (comment above), non-overlap re-checked against all nine open PRs. ConditionalAuthWrapper is exported by app-shell but the console does not use it; apps/console/src/App.tsx mounts AuthProvider directly.", "tests": "Gate union re-run on FINAL head 1639bbe18 after the last push; exit codes captured before any pipe; each verdict quoted from the gate's own output. `pnpm exec vitest run apps/console/ packages/app-shell/src/chrome/` -> 'Test Files 82 passed (82)' / 'Tests 931 passed (931)'. `pnpm turbo run type-check --filter=@object-ui/app-shell --filter=@object-ui/console` -> 'Tasks: 37 successful, 37 total'. `pnpm type-check:e2e` (tsc -p tsconfig.e2e.json) clean, exit 0. `pnpm lint` run IN FULL, not narrowed -> 'Tasks: 47 successful, 47 total', 0 errors (28 pre-existing no-explicit-any warnings, none in my files). check:readme-exports '✅ OK (... 0 unbuilt ...)'; check:control-bytes '✅ OK (scanned 5364 tracked text file(s))'; check:eager-closure '✅ 3233.9 KB gzipped (budget 3266.6 KB, headroom 32.7 KB)'; lint:coverage '✅ 46/46 packages linted, 0 with outstanding errors'; type-check:coverage '✅ 45/46 via type-check, 0 known-broken'; changeset:check '✅ All workspace packages are in the changeset fixed group' + '✅ No changeset declares a major bump'; check:phantom-deps, check:vi-mock-specifiers, check:esm-specifiers, check:self-import, check:docs-route-closure, check:entry-guard, check:i18n-keys, check:doc-fences all exit 0. NOT MEASURED, excluded and re-run rather than reported as red: check:readme-exports first exited 1 with 'its type entry ./dist/index.d.ts is not on disk -- run pnpm build first' for @object-ui/cli and @object-ui/plugin-ai -- a fresh-worktree prerequisite that never reached the gate body; those two were built and the gate re-run to the ✅ above. INSTRUMENT VALIDATION (prove it can see the defect): against unmodified pinned base 7975f2d85, before any fix existed, 9/12 flashed, gaps 36-260ms -- reds on the known-bad tree at or above the card's reported rate. Restores are pinned to that BASE SHA, never origin/main. ACCEPTANCE BAR, stated and hit: the card is honest that n=9 cannot distinguish 4/9 from 3/9, so the bar chosen was 0 flashes over >=60 paired post-fix boots in the cell where the pre-fix build flashes -- at the card's ~1/3 rate, P(0 of 60) = 2.7e-11, 95% upper bound (rule of three) 5.0%. CAMPAIGN, both arms INTERLEAVED inside one process and one browser so load drift lands on both equally, 102 paired boots per arm, 0 NOT MEASURED across all 204: local signed-out / (LoginRedirect) n=60 -> flash 45/60 (75%) vs 0/60; local signed-in / (RootLandingRedirect) n=12 -> 10/12 vs 0/12; local unmatched /console/ (catch-all) n=15 -> 12/15 vs 0/15; wifi 20Mbps/80ms n=12 -> 0/12 vs 0/12 DEGENERATE; fast4g 4Mbps/70ms n=3 -> 0/3 vs 0/3 DEGENERATE. The two throttled cells are reported but NOT pooled into the pixel result: the pre-fix build did not flash there, so before and after agree and they prove nothing about pixels (they still separate 15/15 vs 0/15 on the DOM window). Pooled over the three non-degenerate cells: 67/87 (77%) pre-fix vs 0/87 post-fix, 95% upper bound on the residual 3.4%, Fisher exact p=4.7e-20 on the primary cell alone. On the DOM window across all five cells: 102/102 vs 0/102. Pre-fix gaps 60-396ms (card reported 40-230ms). GHOST-ASSERTION GUARDS on every run: React's first commit observed, DOM settled, expected destination text present in body.innerText, >=1 contentful frame captured -- any failure counts the run NOT MEASURED, never a pass. The two compared builds are genuinely different artifacts (7975f2d85 vs 1639bbe18) served by identical code from two directories, so no cell is degenerate by construction. E2E RED-FIRST: the new e2e/console-boot-indicator.spec.ts case is 6/6 RED on the pre-fix bundle and 6/6 GREEN on this one, failing with 'the viewport was empty for 7 sample(s) spanning ~65ms after React's first commit -- first at t=1351ms on /console/ with the centre hit test landing on <html>'; the three pre-existing tests in that file stay green on both builds. It gates the DETERMINISTIC half deliberately: the flash needs a frame swapped inside the window, but the window itself was present on 100% of pre-fix boots. UNIT REVERSE-VERIFICATION, direction predicted then run: removing <LoadingScreen /> from RedirectWithSplash gives exactly 'Tests 1 failed | 3 passed (4)' -- the splash case reds, the three navigation cases stay green, which is why they are separate cases (a single 'it redirects' assertion survives dropping the splash). Mutation confirmed ON DISK before measuring (HEAD blob 57b2b700 -> mutated 4ecf4e6e, '<LoadingScreen />' occurrences 1 -> 0, non-empty hashes required); restore via `git checkout HEAD -- <abs path>` under an EXIT/INT/TERM trap with absolute paths, PROVEN byte-identical to HEAD with `git diff HEAD` empty. jsdom limits stated in the test header rather than papered over: no compositor and no CSS engine there, so nothing in vitest measures the timing window -- it pins structure only, and the timing claim rests on the browser harness and the e2e gate. CLAUSE 2 (published surface): it DID grow, deliberately, by two symbols -- read off the BUILT entry, not a source grep: packages/app-shell/package.json exports['.'].types -> ./dist/index.d.ts, line 40 re-exports RedirectWithSplash and type RedirectWithSplashProps from ./chrome/index.js; chain is src/index.ts -> chrome/index.ts -> the module, and apps/console consumes it through the package entry. Props kept minimal (to, replace). Nothing skipped, disabled or quarantined; no shrink-only baseline line added.", "open_questions": [], "out_of_scope_findings": [ "filed as #6507: the same LoadingFallback -> null-rendering <Navigate> shape survives at six more gates in packages/app-shell/src/console/ (RequireOrganization x2, RequireAiSurface, RootRedirect, SetupRedirect, AppContent's no-accessible-app bounce) plus SystemRedirect; unmeasured paths outside this card's file face. RootRedirect is byte-for-byte the shape of RootLandingRedirect, where this card measured its widest window -- the console does not mount it, but app-shell publishes it to consumers who assemble their own console." ] }
Generated by Claude Code
- added a commit that references this issue
on Sep 1, 2026
Found while measuring #2628. Different window, independent of that card — #2628 is the gap before
LoadingScreenmounts; this one is a fully-white frame that appears after it has already painted, and it survives the #2628 fix.What was measured
Production build of
apps/console, served locally with the two boot endpoints mocked, driven by Playwright with CDPPage.startScreencastateveryNthFrame: 1. Every captured frame decoded and classified pixel-by-pixel (a frame counts as white when no colour channel falls below 242). Timestamps are relative toperformance.timeOrigin.The frame ledger of one run on
f53a8d0ae, unthrottled localhost:How often
Counting only frames after the first contentful frame, over full-boot runs (the instrument waits for React's first commit, so the whole boot is sampled) — same instrument on both sides, 3 runs per cell:
Gap length 40-230 ms.
That the rate is unchanged by the #2628 fix is expected: that fix covers the window before React's first commit and its indicator is already removed by the time this happens.
What is NOT established
LoadingScreenunmounting before the replacing view has painted, orRouteFader(packages/app-shell/src/chrome/RouteFader.tsx) — but nothing here correlates the frame with a DOM state, so both are guesses. Whoever picks this up should timestamp the DOM alongside the frames rather than start from either name.Why it is filed rather than fixed
#2628 was scoped by triage to tier 1 — the pre-
LoadingScreenwindow only — and explicitly told the dev to file anything else instead of widening. This is that.The repro harness (server + screencast classifier) is not committed anywhere; it is described in full in the #2628 pull request, and
e2e/console-boot-indicator.spec.tslanded there carries the Playwright-side scaffolding a fix for this could extend.