Skip to content

Say something while the app is loading - #60

Open
DanialBeg wants to merge 2 commits into
mainfrom
fix/blank-first-load
Open

DanialBeg wants to merge 2 commits into
mainfrom
fix/blank-first-load

Conversation

@DanialBeg

@DanialBeg DanialBeg commented Oct 1, 2026 •

Copy link
Copy Markdown
Member

"when i initially go to the url i see this, i need to refresh to then see the main page"

The screenshot is the app working as written. That blank parchment rectangle is the loading screen: App.tsx rendered the background and the grain overlay and nothing else — a display: flex; align-items: center container with nothing to centre.

So a load that was merely slow was indistinguishable from one that had died, and reloading was the rational response. It works, because the second load has a warm token. The app was teaching people to refresh it.

What this fixes

A real loading screen, with two rules worth stating:

  • The spinner waits 400ms. A load that resolves in 200ms should show nothing; a spinner flashed that briefly reads as jank, not speed.
  • After 9s it admits it. "Still loading… your connection may have dropped", with a Reload button — the thing people were already doing. That is the error state PRODUCTION-TODO Add Aid Engine, educational content, and real college data #10 asked for.

Profile attempts also get 2.5s / 4s / 6s rather than a flat 6s, because a transient failure is likelier than a genuinely slow query and the old first attempt cost 6.6s before the retry began.

What this does not fix, and why there is nothing to fix

I first wrote the timeout change as a fix for the cold-start wait, claiming the initial fetch raced Supabase's token rotation. That was wrong, and reading the client says so plainly.

In auth-js 2.101.1, __loadSession refreshes an expired session itself before returning it:

const hasExpired = currentSession.expires_at
  ? currentSession.expires_at * 1000 - Date.now() < EXPIRY_MARGIN_MS : false
...
const { data: session, error } = await this._callRefreshToken(currentSession.refresh_token)

and onAuthStateChange waits for initialization and the lock before emitting:

await this.initializePromise
await this._acquireLock(this.lockAcquireTimeout, async () => { this._emitInitialSession(id) })

So INITIAL_SESSION only ever reaches our callback with a valid token. getProfile never races the rotation, and fetchProfile is not where a cold load spends its time.

The seconds go on the refresh round-trip itself — swapping a refresh token for a new access token — which happens before any of our code runs, on a first visit after the access token expired (1h by default) and not on an immediate reload. That matches the reported symptom exactly, and nothing in this repo can shorten it: an authenticated query cannot go out before the token it needs exists.

What we can do is stop showing a blank page during it. That is this PR.

Honest summary

~3s of "Loading your dashboard…" instead of ~7s of nothing. The blank page is gone. The cold-start delay is a required network call and is unchanged.

If you want certainty about the split between that refresh and everything else, the next step is measuring it on a real cold load with a real session, which I cannot do from here. Happy to add the instrumentation if the number matters.

Testing

533 passing, lint and typecheck clean, build succeeds. Four new cases on the timing, since that is what silently regresses: nothing before 400ms, text after, the stuck state with its reload at 9s, and role="status" / aria-live so it is not silence to a screen reader either.

(Force-pushed once to correct the commit message, which asserted the same wrong mechanism. No one else was on the branch.)

@vercel

vercel Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
webapp Ready Ready Preview Oct 1, 2026 11:47pm UTC

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Deploying timeline-prototype with  Cloudflare Pages  Cloudflare Pages

Latest commit: 4ad7b70
Status: ✅  Deploy successful!
Preview URL: https://09e5cbba.timeline-prototype.pages.dev
Branch Preview URL: https://fix-blank-first-load.timeline-prototype.pages.dev

View logs

Opening the app showed an empty parchment rectangle until sign-in resolved,
which on a cold load takes several seconds. Nothing on screen said it was
working, so it read as broken, and the reliable fix was to reload — which
works, because the second load has a warm token. The app was teaching people
to refresh it.

The loading screen had no content: the right background, the grain overlay,
and nothing else. It now has a spinner and a line of text, after a 400ms pause
so a fast load still shows nothing — a spinner flashed for 200ms reads as jank
rather than speed. After nine seconds it says it is taking too long and offers
the reload people were reaching for anyway, which is the error state
PRODUCTION-TODO #10 asked for.

Profile attempts also get 2.5s, 4s, then 6s rather than a flat 6s. A transient
failure is likelier than a genuinely slow query, and the old first attempt cost
nearly seven seconds before the retry began.

What that second change is not: a fix for the cold-start wait. I first wrote it
as one, claiming the initial fetch raced Supabase's token rotation. Reading
auth-js 2.101.1 says otherwise — __loadSession refreshes an expired session
itself, and onAuthStateChange holds the init lock until it has, so
INITIAL_SESSION only ever reaches us with a valid token. The seconds go on that
refresh round-trip, before any of our code runs, and nothing here can shorten
it. Only make it legible, which is what this does.
The claim is that the wait is auth-js swapping a refresh token for a live one
before it hands us a session. That is read off its source, not measured on a
real connection, and the difference matters: if it is right there is nothing
to optimise here, and if it is wrong we have been looking in the wrong place.

Three marks — the app starting, the auth listener firing, the profile landing
— and one console line splitting the total between them. The first figure is
everything before our code runs; the second is our own query, with which
attempt won.

Off unless asked for: any dev server, or `?timing` on the URL so a deployed
preview can be checked without a build. Nothing leaves the browser.

This branch was successfully deployed

1 active deployment
Preview — 4ad7b70f Deployed Oct 1, 2026 by vercel[bot]
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.

1 participant