Skip to content

perf(ui): stabilize snapshot slice identity so sidebar memos hit#34

Merged
danielss-dev merged 1 commit into
mainfrom
perf/sidebar-ref-tree
Jul 6, 2026
Merged

perf(ui): stabilize snapshot slice identity so sidebar memos hit#34
danielss-dev merged 1 commit into
mainfrom
perf/sidebar-ref-tree

Conversation

@danielss-dev

Copy link
Copy Markdown
Owner

Summary

Closes the last open item under TASKS → Performance → Audit follow-ups ("Sidebar: memoize ref-tree builds / leafCount; debounce refreshTree off the status dep").

Root-caused one level down: the sidebar ref-tree builds were already useMemo'd — they rebuilt anyway because refreshSnapshot replaced status/workTree/refs/submodules/meta with freshly deserialized objects on every refresh, so input identity churned on every stage toggle and watcher tick.

  • ui/src/lib/stable.tsstable(prev, next) returns the previous reference when the new value is structurally equal (jsonEqual, early-exit walk so the something-changed case stays cheap). Applied to every snapshot-fed slice and the standalone refreshStatus/refreshRefs/refreshTree/refreshSubmodules setters; the per-tab meta patch also keeps tabs identity when the tab meta is unchanged. Unchanged slices keep their identity, so the sidebar ref trees, palette index, and Files PierreTree stop rebuilding — and their zustand selector subscribers stop re-rendering — for identical data.
  • Sidebar Files-tab effect — the status dep is removed rather than debounced: since the repo_snapshot batch, every status-changing path already refreshes workTree from the same statuses walk, so the effect's repo_tree IPC was a redundant second walk of the working tree on every stage toggle. It now runs only on tab show / repo switch (the lazy first load).
  • Folder leaf counts — precomputed once per tree build (countLeaves fills TreeNode.leaves) instead of recursing per folder row on every render.

Audited all consumers before stabilizing: nothing keys effects off these slices' identity as a "refresh tick" — every dependent is a pure memo/callback, so keep-if-equal is behavior-preserving.

Test plan

  • tsc --noEmit — clean
  • vitest run — 198 tests pass (+13 new for jsonEqual/stable: primitives, null vs undefined, arrays, nested objects, key-set mismatch, identity-keeping semantics)
  • vite build — clean

🤖 Generated with Claude Code

The sidebar ref-tree builds were already memoized, but refreshSnapshot
replaced status/workTree/refs/submodules/meta with freshly deserialized
objects on every refresh, so input identity churned on every stage
toggle and watcher tick and every memo rebuilt for identical data.

- lib/stable.ts: stable(prev, next) keeps the previous reference when
  the new value is structurally equal (jsonEqual, early-exit walk).
  Applied to every snapshot-fed slice and the standalone refreshStatus/
  refreshRefs/refreshTree/refreshSubmodules setters; the per-tab meta
  patch also skips when the tab meta is unchanged.
- Sidebar: drop the status dep from the Files-tab effect — since the
  repo_snapshot batch, every status-changing path already refreshes
  workTree from the same statuses walk, so the effect IPC-walked the
  working tree a second time on every stage toggle. It now runs only on
  tab show / repo switch (the lazy first load).
- Sidebar: precompute folder leaf counts once per tree build
  (countLeaves fills TreeNode.leaves) instead of recursing per folder
  row on every render.

Closes the last open item under TASKS - Performance - Audit follow-ups.

Verified: tsc, vitest (198, +13 stable), vite build.
@danielss-dev danielss-dev merged commit e61c8cb into main Jul 6, 2026
2 checks passed
@danielss-dev danielss-dev deleted the perf/sidebar-ref-tree branch July 6, 2026 20:08
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