hermes-agent/apps/desktop/src/lib/use-session-slice.ts
Brooklyn Nicholson 41dd895e12 perf(desktop): stop the statusbar re-rendering per streaming token
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
2026-07-26 14:21:34 -05:00

63 lines
2.8 KiB
TypeScript

import { useCallback, useRef, useSyncExternalStore } from 'react'
interface SliceStore<T> {
get(): Record<string, T[] | undefined>
listen(listener: () => void): () => void
}
// Stable empty result so an absent key never yields a fresh array (which would
// defeat the snapshot bail-out and re-render on every store write).
const EMPTY: readonly never[] = []
/**
* Subscribe to ONE session's slice of a `Record<sessionId, T[]>` nanostore,
* re-rendering only when *that* slice's reference changes — not on writes to
* other sessions. The map reference churns on every cross-session update, so a
* plain `useStore(map)` re-renders all consumers globally; reading `map[key]`
* through `useSyncExternalStore` bails out whenever the keyed array is
* unchanged (the stores update immutably per key). Returns a shared empty array
* when the key is null/absent.
*
* Note: only helps stores whose per-key arrays are referentially stable across
* unrelated writes (plain atoms with immutable per-key updates). A `computed`
* that rebuilds the whole map churns every slice — use a presence/edge selector
* there instead.
*/
export function useSessionSlice<T>(store: SliceStore<T>, key: string | null): T[] {
return useSyncExternalStore(
onChange => store.listen(onChange),
() => (key ? (store.get()[key] ?? (EMPTY as unknown as T[])) : (EMPTY as unknown as T[]))
)
}
interface ReadableStore<T> {
get(): T
listen(listener: () => void): () => void
}
/**
* Subscribe to a SCALAR derived from a hot store, re-rendering only when that
* scalar changes by `Object.is` — not on every write to the store it came from.
*
* `useStore($someHotStore)` bails out on reference equality alone, so a store
* republished per streaming token re-renders every consumer even when the two
* or three fields they actually read are identical. `$sessionStates` is the
* canonical case: it is republished on every message delta, so a component
* reading only `busy` or `turnStartedAt` off it pays for the whole transcript's
* churn.
*
* `select` must return a PRIMITIVE (or a referentially stable value). Returning
* a fresh object or array defeats the bail-out and reintroduces the churn this
* exists to remove — derive one scalar per call instead.
*/
export function useStoreSelector<T, S>(store: ReadableStore<T>, select: (value: T) => S): S {
// `select` is read through a ref so an inline arrow at the call site doesn't
// resubscribe on every render; useSyncExternalStore re-reads the snapshot on
// each render anyway, so the latest selector is always applied.
const selectRef = useRef(select)
selectRef.current = select
const subscribe = useCallback((onChange: () => void) => store.listen(onChange), [store])
return useSyncExternalStore(subscribe, () => selectRef.current(store.get()))
}