perf(sidebar): skip full session-list rebuild when the payload is unchanged (#5455) - #5467
3 commits merged into
Conversation
…hanged (nesquena#5455) The 30s streaming poll and the gateway poll re-applied /api/sessions and ran a full sidebar DOM rebuild (list.innerHTML='' + one node tree per row) on every tick, even when the payload was byte-identical to what was already on screen — the common case on an idle sidebar. Add a signature over the applied rows + projects + coarse display state in _applySessionListPayload and skip renderSessionListFromCache() when it matches the last rendered signature and no entrance animation is pending. A streaming row's fields advance each poll so its signature changes and it still repaints; client-only display toggles (search, active, density, select, lineage expansion) render through their own paths. The per-refresh INFLIGHT cleanup is still run on the skip path. The 60s relative-time refresh and all other render triggers call renderSessionListFromCache directly and are unaffected. Output-neutral: a skip only happens when the render would produce the same DOM. node --check + ESLint runtime guard clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
| Filename | Overview |
|---|---|
| static/sessions.js | Adds _sessionListRenderSignature() and a sig-based skip gate inside _applySessionListPayload to avoid full DOM rebuilds on idle polls; the skip is correctly guarded by animation/rename/action-menu state but _lastSessionListRenderSig is never reset by _invalidateSessionListRenders(), leaving a window where a profile-switch to an identically-shaped list (e.g. two empty profiles) skips the post-skeleton render. |
| tests/test_webui_external_refresh_frontend.py | Adds a structural test verifying that _sessionListRenderSignature uses a slimmed session-key list rather than the raw _allSessions object, protecting against future schema additions silently breaking the skip. |
| tests/test_issue4766_sidebar_source_pushdown.py | Backfills the new globals (_sessionListRefreshAnimationPending, _lastSessionListRenderSig, _renamingSid, _sessionActionMenu, mocked _sessionListRenderSignature / _purgeStaleInflightEntries) into the test harness so existing pushdown tests continue to pass with the new skip gate in _applySessionListPayload. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[_applySessionListPayload called\npoll / WS push] --> B[Update _allSessions / _allProjects\ncounts, filters]
B --> C[_sessionListSkeletonActive = false]
C --> D{_canRenderNow?\n!renamingSid && !actionMenu}
D -- No --> G[renderSessionListFromCache\nno-ops per own guards]
D -- Yes --> E[Compute _renderSig\n_sessionListRenderSignature]
E --> F{animationPending?\nor sig null?\nor sig != last?}
F -- Skip conditions NOT met\nidentical idle payload --> H[_purgeStaleInflightEntries\nreturn early - DOM untouched]
F -- Sig changed or\nanimation pending --> I[_lastSessionListRenderSig = _renderSig]
I --> J[renderSessionListFromCache\nfull DOM rebuild]
J --> K[_sessionListRefreshAnimationPending = false\nanimate FLIP if pending]
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
A[_applySessionListPayload called\npoll / WS push] --> B[Update _allSessions / _allProjects\ncounts, filters]
B --> C[_sessionListSkeletonActive = false]
C --> D{_canRenderNow?\n!renamingSid && !actionMenu}
D -- No --> G[renderSessionListFromCache\nno-ops per own guards]
D -- Yes --> E[Compute _renderSig\n_sessionListRenderSignature]
E --> F{animationPending?\nor sig null?\nor sig != last?}
F -- Skip conditions NOT met\nidentical idle payload --> H[_purgeStaleInflightEntries\nreturn early - DOM untouched]
F -- Sig changed or\nanimation pending --> I[_lastSessionListRenderSig = _renderSig]
I --> J[renderSessionListFromCache\nfull DOM rebuild]
J --> K[_sessionListRefreshAnimationPending = false\nanimate FLIP if pending]
Reviews (3): Last reviewed commit: "fix: narrow sidebar render-skip signatur..." | Re-trigger Greptile
7a700b9
|
Shipped in v0.51.844 — thanks @ai-ag2026! 🎉 The sidebar render-skip on idle polls is live (for #5455): the session list now skips its full DOM rebuild when a signature of the render inputs is unchanged, and rebuilds whenever anything visible changes. This is a correctness-sensitive change (a false-skip = a stale sidebar), so it went through a deep dual gate and I fixed every finding on the release branch before shipping (all credited to you):
|
… when payload unchanged (nesquena#5455)
…/error-banner DOM Codex re-gate found 2 more false-skips (DOM state OUTSIDE the signature path): - CORE: profile-switch skeleton stays visible if the new payload signature matches the last one (empty/same-shaped profile) — skip fires over the skeleton. - SILENT: stale 'Could not load conversations' banner persists when a transient fetch failure heals with identical rows. Fix: capture _hadSessionListSkeleton + _hadSessionListLoadError BEFORE clearing them, and disallow the identical-signature skip when either was set. + regression test. (messages.js:1462 finding is a base-comparison artifact for nesquena#5472/nesquena#5479/nesquena#5484, not in nesquena#5467's diff.)
Problem
The 30s streaming poll and the gateway poll re-applied
/api/sessionsand ran a full sidebar DOM rebuild (list.innerHTML=''+ one node tree per row) on every tick — even when the payload was byte-identical to what was already on screen, the common case on an idle sidebar. Part of the #5455 latency work.Change
Add a signature over the applied rows + projects + coarse display state in
_applySessionListPayload, and skiprenderSessionListFromCache()when it matches the last rendered signature and no entrance animation is pending. A streaming row's fields advance each poll, so its signature changes and it still repaints. Client-only display toggles (search, active, density, select, lineage expansion) render through their own paths. The per-refreshINFLIGHTcleanup still runs on the skip path, and the 60s relative-time refresh plus every other render trigger callrenderSessionListFromCachedirectly and are unaffected.Behavior
Output-neutral: a skip only happens when the render would produce the same DOM.
Verification
In-browser: over an idle window, applied-payloads > DOM-rebuilds (redundant rebuilds skipped); sidebar still updates immediately on real changes (new session, title change, session switch, streaming indicator).
node --check+ ESLint runtime guard pass.Refs #5455.
🤖 Generated with Claude Code