perf(desktop): stop the statusbar re-rendering per streaming token - #72163
Merged
Conversation
The statusbar subscribed to `$focusedSessionState` — a projection of `$sessionStates`, which is republished on every message delta — but reads only three fields off it. Every token therefore re-ran useStatusbarItems and rebuilt all ~9 item objects, and since StatusbarItemView was not memoized, one changed item (the running timer) re-rendered the whole bar. Adds `useStoreSelector` beside the existing `useSessionSlice`, the same narrowing idea for a scalar instead of a keyed array: subscribe to the store, but bail out unless the selected value changes. Applies it to the three fields the statusbar actually reads, and memoizes StatusbarItemView. Measured over five concurrent streaming tabs (`render-churn`): total renders 78,385 -> 21,701 (-72%) wasted renders 10,432 -> 6,112 (-41%) TooltipContent 2,304 -> 143 (-94%) StatusbarItemView 2,174 -> 0
Contributor
૮ >ﻌ< ა ci reviewran on 41dd895 ℹ️ InfoDesktop E2E visual evidence · View test artifacts · View job1 visual diff. inline evidence upload failed. Failed to upload diff-665a0833239e-onboarding-overlay-diff.png with gh image (exit code 1): Error uploading /home/runner/work/_temp/e2e-evidence/diff-665a0833239e-onboarding-overlay-diff.png: step 0 (get upload token): uploadToken not found on repo page — do you have write access to NousResearch/hermes-agent? (or, if NousResearch enforces SAML SSO, authorize at https://github.com/orgs/NousResearch/sso) |
randlee
pushed a commit
to randlee/hermes-agent
that referenced
this pull request
Aug 11, 2026
…nder-waste perf(desktop): stop the statusbar re-rendering per streaming token
randlee
pushed a commit
to randlee/hermes-agent
that referenced
this pull request
Aug 11, 2026
…ng token ModelPickerOverlay subscribed to $focusedSessionState whole — a projection of $sessionStates, republished on every message delta — to read two fields that essentially never change (model, provider). The overlay is mounted app-wide and unconditionally renders the un-memoized ModelPickerDialog (closed), so the focused session's stream re-ran the dialog's full hook body ~30x/s. Same defect class and same fix as the statusbar (NousResearch#72163): select each scalar through useStoreSelector so unchanged values bail out.
randlee
pushed a commit
to randlee/hermes-agent
that referenced
this pull request
Aug 11, 2026
NousResearch#72163 narrowed $focusedSessionState but left two whole-store reads in the same hook paying the same price: - $subagentsBySession: only two COUNTS are rendered, but the whole-map subscription re-ran the hook (rebuilding all ~9 statusbar items) on every subagent progress tick in any session. Select the two scalars. - $sessions: only one row's started_at is read, but any session-list write (title update, poll refresh, archive) re-ran the hook. Select the one scalar.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
First fix found with the render-churn counters from #71925. Cuts total renders during multi-tab streaming by 72%.
The bug
The statusbar subscribed to
$focusedSessionState— a projection of$sessionStates, which is republished on every message delta — but reads only three fields off it (busy,usage,turnStartedAt).@nanostores/react'suseStorebails out on reference equality, so a republished state object re-ranuseStatusbarItemsper token, rebuilding all ~9 statusbar item objects.StatusbarItemViewwasn't memoized, so one genuinely-changed item (the running timer, ticking every second) re-rendered the entire bar. Each of those items carries a tooltip, which is whyTooltipContentwas the single worst component in the whole app at 2,048 wasted renders of 2,304.The statusbar looked innocent —
StatusbarSurfaceis alreadymemo()'d with a comment explaining it's isolated so "any statusbar-only churn re-renders the bar alone." That isolation was real and still didn't help, because the churn was inside the boundary.The fix
Adds
useStoreSelectorbeside the existinguseSessionSliceinlib/use-session-slice.ts— the same narrowing idea that file already documents, for a scalar instead of a keyed array. Subscribe to the store, but bail out unless the selected value actually changes.Applied to the three fields the statusbar reads, plus
memo()onStatusbarItemView. No behavior change; 30 shell tests pass.Measured
Five concurrent streaming tabs,
node scripts/perf/run.mjs render-churn --spawn --tiles 5 --tokens 240:TooltipContentStatusbarItemViewsidebar_rendersstays at 6 with 0 wasted — the sidebar was already clean and this doesn't disturb it.Note on the selector contract
selectmust return a primitive or a referentially stable value; returning a fresh object or array defeats the bail-out and reintroduces the churn. That's documented on the function.usageis passed through as an object deliberately — it's replaced wholesale rather than mutated and only changes when the backend reports new usage, so its reference is a valid bail-out key on its own.Remaining waste (
Block/Ctat 880/880 each, 542ms combined) is in the transcript renderer under minified component names — separate PR.