oficina / docs Public

flash-stale-paint: the flash is a reconnect, and two instruments that lied about it closed

On branch flash-stale-paint

Paul: "There's still a flash. Are you able to test this on a real browser?"

Yes — and doing so showed the previous branch fixed a real defect that was not this one.

What the flash actually is

A hidden tab closed its stream on the spot (noteHidden() { closeStream() }). Coming back reopened it and fetched the catch-up, and that round trip cannot beat the paint. The browser draws the document it already has — the version from before you left — and the corrected text lands a frame later. The paragraph visibly rewrites itself and reflows across lines.

On loopback the gap is 18ms. Over a real network it is a whole round trip, which is why Paul sees it plainly on the deployed host and why a local capture nearly missed it.

Nothing about making the reconnect faster removes this. The paint happens before any round trip can possibly return.

The measurement, both ways round

fixtures/flash-frames.mjs drives two real tabs: type in B, switch away, edit in A, switch back.

| | DOM has the edit while hidden | painted frames showing old text | |---|---|---| | before | false | 1 | | after | true | 0 |

The "1" is the flash, on screen, counted. The harness was proven able to see it — run against the old build first — before it was used to say it was gone.

The fix

A hidden tab shortens its deadline to 30s instead of closing. A switch away and back never disconnects, so there is nothing to catch up and the first painted frame is already current.

This serves §7.3 better than before, not worse: 30s is four times more aggressive than the 120s a merely idle visible tab is already allowed. A document left open on Friday drops its stream half a minute after you look away.

Two instruments that lied, both now recorded

Worth more than the fix, because both would have been believed again.

Counting DOM mutations measures a proxy. The previous branch drove 23 redundant attribute writes to 0 and reported the flash fixed. Those writes were real and worth removing — but a mutation count can go to zero while what a person sees is untouched, because a flash is a paint.

Chromium hands a screencast the last composited surface of a hidden tab. It arrives as an ordinary frame, indistinguishable from a paint. The first version of this harness read one as the flash — and it was delivered 787ms before the reveal, while the tab was provably still hidden. Adding a deliberate gap between starting the cast and revealing the tab is what exposed it.

The honest instrument is a requestAnimationFrame sampler: rAF does not run in a hidden tab, so its first entry is the first frame actually drawn. The screencast additionally only emits when the page happens to paint, and could not resolve a 20ms window at all — it showed an identical frame sequence for the broken and fixed builds.

Verification

Go 6/6 packages, JS 97/97 (two new tests, both confirmed to fail against the immediate close on the assertions that name the defect), wire-gate exit 0, browser drive 44/44.

Merged as 779db9b and live.

The verification chain, and it earned its keep this time — link 3 failed:

  • CI passed on 779db9b, the merge sha.
  • The committed bundle reproduces: rebuilding from fixtures/ leaves git status clean.
  • x-carlos-version read 779db9b on the live host — and it was wrong. Fetching /static/editor.879cf51cc4b8e027.js returned 404 while the previous bundle still returned 200, which is the running binary saying plainly that it was still the old one. carlos deploy had already printed "live" on the strength of that header.
  • carlos restart closed it. The new bundle now returns 200 at 336161 bytes, cmp-clean against the file the measurement was taken on, and the old one 404s. The minified grace constants are both in the served bytes: 3e4 (the 30s hidden grace) beside 12e4 (the 120s idle deadline).

Worth recording as a project fact rather than a one-off: the version header, carlos deploy's own "live", and carlos channels can all three agree and all three be wrong. Only fetching the content-hashed asset distinguishes "the channel points at it" from "the process is running it", because that filename does not exist in the old binary at all.

Because the bundle name is content-hashed, no returning tab can serve the old JavaScript from cache — the file it would ask for is gone.

Closing.