Skip to content

perf(desktop): prefetch the channel window on sidebar hover intent - #6534

Closed
Maxwellimus wants to merge 2 commits into
fix/switch-visual-stabilityfrom
perf/sidebar-hover-prefetch
Closed

perf(desktop): prefetch the channel window on sidebar hover intent#6534
Maxwellimus wants to merge 2 commits into
fix/switch-visual-stabilityfrom
perf/sidebar-hover-prefetch

Conversation

@Maxwellimus

@Maxwellimus Maxwellimus commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Based on perf/switch-tracing — retarget to main after it merges. (Shares the message-window queryFn hunk with the tracing instrumentation.)

A deliberate hover on a sidebar channel row (100ms dwell — pointer scrubbing never fires) warms that channel's message window, so the subsequent click paints from cache instead of paying the relay fetch on the switch path. Live traces measured that fetch at 180–310ms per cold channel entry on a real community — this moves it into the hover dwell, ahead of the click.

The window's query options move into a shared factory so the prefetched entry is byte-identical to what the mounted query reads; the prefetch respects the window's staleTime (hovering a fresh channel never refetches) and dedupes with in-flight fetches. Forum channels are excluded, matching the window query's own gating.

The post-subscribe window refresh cancels an in-flight prefetch rather than deduping onto it: a prefetch reaches the relay before the live subscription exists, so its snapshot can miss events that landed in between. A cold mount fetch is still parked on the persisted-head gate, carries no such gap, and must keep deduping (#6455's one-fetch contract) — channelWindowPrefetches.ts is the marker that tells the two apart.

The mock bridge gains a head-fetch probe (cursorless window reads) and a regression spec asserts both directions: dwell fetches exactly once before any click; scrubbing across rows fetches nothing.


Recreates #6459. It was approved and squash-merged into its stacked base perf/switch-tracing, but a force-push of that base (carrying the review-requested P1 fixes for #6455) raced the merge and overwrote the squash commit — so the merged content never survived on the base branch. Same content, same base; still stacked on #6455 and should merge after it.

A deliberate hover on a sidebar channel row (100ms dwell, so pointer
scrubbing never fires) warms that channel's message window, making the
subsequent click a cache hit instead of paying the relay fetch on the
switch path.

The window query options move into a shared factory so the prefetched
entry is byte-identical to what the mounted query reads; the prefetch
respects the window's staleTime, so hovering a fresh channel never
refetches. The mock bridge gains a head-fetch probe (cursorless window
reads) so the regression spec can assert dwell-fetches-once /
scrub-fetches-never.

Signed-off-by: Max Lampert <maxwell@squareup.com>
@Maxwellimus
Maxwellimus force-pushed the perf/sidebar-hover-prefetch branch from b90987f to a88994e Compare August 27, 2026 05:17
@Maxwellimus
Maxwellimus changed the base branch from perf/switch-tracing to fix/switch-visual-stability August 27, 2026 05:17
Converging review found this at P1, two lenses independently, both with
probes against the installed query-core source. Reproduced here.

The prefetch marker records that a channel had a prefetch requested, not
which fetch is in flight. On a cold channel `prefetchQuery` does not start a
fetch at all — TanStack dedupes onto the request already running (query.js
honours `cancelRefetch` only when `state.data !== undefined`). So when the
hover dwell fires after the click, which is the ordinary case for a decisive
click inside the 100ms dwell, the marker labels the mounted timeline's own
fetch a "prefetch", and the post-subscribe refresh cancelled it.

Probe, real modules and a real QueryObserver:
  no hover:           { calls: 1, mountFetchAborted: false }
  hover then click:   { calls: 2, mountFetchAborted: true }

That is two sequential relay round trips with the skeleton held across both,
making a hovered channel slower to open than an unhovered one — on the exact
path this stack exists to speed up.

The refresh now waits for the in-flight fetch instead of cancelling it. The
gap it was closing still closes: the invalidate below re-reads the window
after the subscription exists, it just no longer discards the round trip the
user is watching. #6455's one-fetch contract for a cold channel with no
prefetch is unchanged.

The guard's own comment claimed a cold mount fetch "is still parked on the
hydration gate" and so could not be affected. That is false in steady state —
`channelHeadHydration` is an already-resolved promise after boot.

Adds the regression test; it fails on the cancelling version.

Signed-off-by: Max Lampert <maxwell@squareup.com>
@Maxwellimus

Copy link
Copy Markdown
Contributor Author

Closing this — dropping the hover prefetch from the perf stack.

Why: nothing in the stack measures its benefit. The high-membership benchmark has no coverage of the hover/prefetch path at all (it measures warm switches, where the window is already cached and the prefetch is a no-op), and sidebar-hover-prefetch.spec.ts only asserts that a fetch happens on dwell — never that the prefetch is actually used, i.e. that the click paints faster or from cache. So this is a latency feature with no latency evidence.

The costs are concrete:

  • It only helps cold channels where the user dwells ≥100ms first. A decisive click at a known target never reaches the dwell.
  • It writes the persisted channel-head cache, so channels merely hovered evict genuinely-visited ones from the next boot's instant-paint set — it degrades a different perf feature.
  • After the fix in f051772 it costs an extra background relay round trip per hovered-then-clicked channel.
  • A converging review found that before that fix, the prefetch marker caused the post-subscribe refresh to cancel the mounted timeline's own in-flight fetch, making a hovered channel slower to open than an unhovered one — on the exact path this was meant to speed up.

For ~170 lines plus marker lifecycle, community-reset wiring and cross-PR coupling, that is not a good trade without a measured win.

The branch perf/sidebar-hover-prefetch is left in place, so this is recoverable if a measurement later justifies it. The remaining stack is #6455 (benchmark + extractions) → #6524 (skeleton flash + thread-summary reflow).

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