Skip to content

perf(navigation): skip the shell render when a visit opens - #2992

Draft
kalvinnchau wants to merge 2 commits into
mainfrom
peon/sw-opened-view
Draft

kalvinnchau wants to merge 2 commits into
mainfrom
peon/sw-opened-view

Conversation

@kalvinnchau

Copy link
Copy Markdown
Contributor

Problem

A channel switch renders the whole app shell three times. The third render comes from navigation.complete({ status: "opened" }). The controller then publishes a new snapshot that differs only in status, and the shell re-renders about 470 components. The shell never reads "opened": App.tsx and navigation.ts only check status === "failed".

Change

  • shellSnapshot(navigation) in src/app/navigation.ts is the useSyncExternalStore getter for useAppNavigation. When a snapshot differs from the one last shown only by "opening" → "opened", it keeps showing the opening snapshot. Every other change is shown as before, including history updates that land on an opened visit.
  • ChannelsPage read navigator.snapshot().status during render and relied on that shell render to notice a settled visit. This mattered only when the relay is in error and retrying ("Connecting…" vs "Can't connect. We'll keep trying."). It now subscribes, with a getter that is true only in that case, so ordinary switches don't re-render it.

No behavior change

  • Every in-repo reader of navigation status either subscribes or reads only "failed".
  • ChannelsPage.test.tsx no longer re-renders the page by hand after the visit completes. It now fails without the subscription.
  • A new navigation.test.tsx case checks that an opened visit keeps the shell's snapshot and that a later history change still reaches the shell. It fails if the getter swallows anything beyond the status change.
  • Six partial Navigation test mocks gain subscribe.

A plugin page that reads navigation.snapshot().status during render without subscribing no longer gets a free re-render at "opened". No in-repo page does this.

Evidence

The warm channel-switch probe:

  • Chromium, 4× CPU throttle.
  • The target channel was already loaded.
  • Dense short-message history.
  • Pointer modality.

Main 9a7c1d228 was compared against this change. 2 Linux and 2 macOS boxes each ran 8 interleaved rounds, giving 24 samples per variant per box. Medians are below, with Mann–Whitney p ≤ 3e-5 for every cell.

box script first rows LoAF blocking
Linux 1 190 → 168 ms (−11.6%) 243 → 216 ms (−11.0%) 161 → 139 ms (−13.7%)
Linux 2 183 → 158 ms (−13.5%) 235 → 212 ms (−9.8%) 152 → 130 ms (−14.4%)
macOS 1 118 → 108 ms (−7.9%) 151 → 140 ms (−7.3%) 70 → 59 ms (−16.2%)
macOS 2 117 → 106 ms (−8.7%) 151 → 139 ms (−8.1%) 70 → 58 ms (−16.6%)

Local tests run:

  • vitest for src/bundled/channels/ (1135 passed), src/app/navigation.test.tsx and the other navigation/App suites, src/features/navigation/controller.test.ts and attachment-panes.test.tsx.
  • tsc --noEmit and biome check.

🤖 Generated with Claude Code

kalvinnchau and others added 2 commits November 7, 2026 13:32
an attempt that opens only changes its status from opening to opened, and
the shell reads only failed. useAppNavigation now keeps the opening snapshot
for that transition, sparing a render of the whole app shell per channel
switch. channelspage read the visit status during render and relied on that
render, so it now subscribes to the one case it uses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Kalvin Chau <kalvin@block.xyz>
server-rendered pages call useSyncExternalStore without a client store, so
channelspage needs getServerSnapshot; pass the same getter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Kalvin Chau <kalvin@block.xyz>
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