Skip to content

Stop the page flickering through states it is not in - #44

Merged
botre merged 2 commits into
masterfrom
fix/hydration-flicker
Aug 8, 2026
Merged

Stop the page flickering through states it is not in#44
botre merged 2 commits into
masterfrom
fix/hydration-flicker

Conversation

@botre

@botre botre commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator

Investigates the flicker on refresh. Three separate causes, all the same shape: the server-rendered markup disagreed with what the component initialises to, so the first paint showed something the page was never in.

x-cloak was doing nothing

Alpine only removes the attribute once it initialises. The CSS rule that hides it has to be supplied by the app, and it never was — so [x-cloak] was inert. The delete confirmation and the Show more control were painted on every load and then hidden a moment later.

That is the delete-warning flicker.

The connection indicator claimed to be Live before connecting

connection initialises to "connecting", but the fallback text said Live and the dot carried no colour class at all. Every load ran: colourless + "Live" → amber + "Reconnecting…" → green + "Live". Two visible transitions to reach the truth.

The fallback now states the connecting state, and the label reads Connecting… rather than Reconnecting, because on first load nothing has dropped.

The results counter rendered a wrong number

Its fallback was 0 results, so a page holding three requests showed "0 results" until hydration. A count nobody has computed is unknown, not zero — it renders empty.

One bug found while fixing the above

Giving the dot a static bg-pending fallback did not work the way it looked. Alpine's string :class syntax adds the computed class without removing the static one, so a live connection carried bg-pending and bg-live together and stylesheet order picked the winner: green label, amber dot. Caught it in a screenshot after the "fix". Object syntax toggles both correctly.

Also: a guard for the asset versioning

The previous PR fixed the stale-stylesheet problem but left the list of versioned assets maintained by hand. Adding a script and forgetting to wrap it silently reintroduces the same failure for that file — invisible locally, visible only at a CDN edge after a deploy.

Three mechanical checks now cover it: every first-party stylesheet and script referenced from a template goes through the asset helper; every path the templates version is in the startup list; every path in that list exists on disk. Verified by introducing both mistakes and confirming each fails with a message naming the file and the fix.

Verification

go vet, gofmt, go build, unit tests, prettier --check . clean, detector clean, Playwright 39/39. Checked in the browser under 20x CPU throttling and Slow 3G to observe the pre-hydration frame, and confirmed after load that the dot's colour and its label agree and that both cloaked elements compute to display: none.

🤖 Generated with Claude Code

https://claude.ai/code/session_01X8JeP7AQwpd2coSfwbMopt

botre added 2 commits August 8, 2026 18:44
Versioning static asset URLs closed the caching hole, but the list of assets to
version is maintained by hand. Adding a script to a template and forgetting to
wrap it reintroduces the same failure for that file, and nothing about it is
visible locally: the page works, and the damage appears only at a CDN edge after
the next deploy.

Three checks, all mechanical. Every first-party stylesheet and script referenced
from a template goes through the asset helper. Every path the templates ask the
helper to version is in the list that gets hashed at startup. Every path in that
list exists on disk, since the helper falls back to an unversioned URL when it
cannot read a file.

Verified by introducing both mistakes and confirming each fails with a message
that names the file and the fix.

Claude-Session: https://claude.ai/code/session_01X8JeP7AQwpd2coSfwbMopt
Three separate causes, all of them the server-rendered markup disagreeing with
what the component initialises to.

x-cloak was doing nothing. Alpine only removes the attribute; the rule that hides
it has to be supplied, and it never was. The delete confirmation and the Show
more control were therefore painted and then hidden on every load.

The connection indicator claimed to be Live before anything had connected. The
component starts in the connecting state, so the fallback says so now, and the
label reads Connecting rather than Reconnecting because nothing has dropped yet.

The results counter rendered "0 results" before any request had been counted. A
count nobody has computed is unknown, not zero, so it renders empty.

The dot's class binding also had to change shape: as a string expression Alpine
adds the computed class without removing the static fallback, so a live
connection showed a green class and an amber dot together and stylesheet order
decided which won. Object syntax toggles both.

Claude-Session: https://claude.ai/code/session_01X8JeP7AQwpd2coSfwbMopt
@botre
botre merged commit 2393a7a into master Aug 8, 2026
3 checks passed
@botre
botre deleted the fix/hydration-flicker branch August 8, 2026 16:49
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