Skip to content

perf: session tab switching performance regression (PR #103) #106

@grimmerk

Description

@grimmerk

Summary

After PR #103 (VS Code session support), tab switching feels slightly slower than before, especially Sessions → Projects. Confirmed by comparing yarn start (PR branch) vs installed v1.0.66 (pre-PR) after a clean macOS restart.

Symptoms

  • Sessions → Projects tab switch: noticeably slower (most visible)
  • Projects → Sessions: possibly slower but less certain
  • Overall UI feels slightly less responsive during session tab interactions

Root Cause Analysis

1. Increased React re-renders per fetchClaudeSessions() call

Priority: High | Confidence: High

Each fetchClaudeSessions() triggers ~12-15 state updates (vs ~8 pre-PR):

State update Pre-PR Post-PR Source
setAllSessions (Step 1) 1 1 Initial load
setAssistantResponses (Step 2) 1 1 loadLastAssistantResponses
setAllSessions + setSessions (Step 3) 1 1 Active session merge
setTerminalApps (Step 3) 1 1 Terminal detection
setAssistantResponses (Step 3 VS Code) 0 1 Pre-loaded VS Code responses
setAllSessions + setSessions (Step 5) 0 1 Closed VS Code merge
setAssistantResponses (Step 5) 0 1 Pre-loaded closed responses
setCustomTitles + setBranches + setPrLinks (Step 4) 1 1 CLI enrichment
setCustomTitles + setBranches + setPrLinks (VS Code enrichment) 0 1 VS Code enrichment

Each state update triggers a re-render of ~100 session items.

Possible fixes:

  • Batch state updates using React.unstable_batchedUpdates or flushSync
  • Combine multiple state variables into a single state object (reduces render count)
  • Use React.memo on session item components to skip unchanged items
  • Move VS Code session state (assistantResponses for VS Code) into the same update as the merge

2. Background async tasks still running when switching tabs

Priority: Medium | Confidence: Medium

When the user switches Sessions → Projects, background tasks from fetchClaudeSessions() may still be completing:

  • loadLastAssistantResponses (100 sessions × tail -n 100 | grep) — ~150ms
  • scanClosedVSCodeSessions (218 files × 4KB read + VS Code sessions × head/tail/grep) — ~100ms
  • loadSessionEnrichment (100+ sessions × 4 grep each) — ~200ms

These spawn child processes that consume CPU even after leaving the Sessions tab.

Possible fixes:

  • Cancel pending async tasks on tab switch (abort controller pattern)
  • Lower priority for enrichment loads (requestIdleCallback)
  • Reduce process spawning by reading files in Node.js instead of shell commands

3. Closed VS Code JSONL scan (synchronous file reads)

Priority: Medium | Confidence: Medium

scanClosedVSCodeSessions() synchronously reads first 4KB of each JSONL file to check entrypoint:

for (const f of files) {
    const fd = fs.openSync(filePath, 'r');
    fs.readSync(fd, buf, 0, 4096, 0);
    fs.closeSync(fd);
}

This blocks the main Electron process for ~50ms (218 files). While short, it blocks IPC and window management during that time.

Possible fixes:

  • Use async file reads (fs.promises.open + read)
  • Move to a worker thread
  • Increase cache TTL from 30s to 60-120s (reduce scan frequency)

4. fs.watch debounce may be too short

Priority: Low | Confidence: Medium

Current debounce is 50ms. Log analysis showed events still firing 3-4 times per status change. Each fire reads all status files + sends IPC.

Possible fixes:

  • Increase debounce to 100-200ms
  • Track last-processed timestamp to skip redundant reads

5. Session item rendering complexity

Priority: Low | Confidence: Low

Each session item now renders:

  • Highlighter component for project name, custom title, branch, terminal badge, PR badge (5 instances even with empty search)
  • PR badge with URL match detection
  • Status dot with animation

Pre-PR each item had fewer Highlighter instances and no PR URL matching.

Possible fixes:

  • Skip Highlighter instantiation when search is empty (return plain text)
  • Memoize session items with React.memo
  • Virtualize the list (only render visible items)

Quantified Performance: Shell Exec vs Native Reads

Current approach spawns ~530 child processes per fetchClaudeSessions() call. Replacing exec('tail | grep') with direct file reads eliminates process spawn overhead.

Operation Shell exec (current) Node.js fs.readFile Rust native module
100 sessions assistant msg ~150ms, 100 processes ~25ms, 0 processes ~5ms
100 sessions enrichment ~200ms, 400 processes ~30ms, 0 processes ~8ms
VS Code active (10 sessions) ~50ms, 30 processes ~10ms, 0 processes ~2ms
JSONL scan (218 files) ~50ms, 0 processes ~50ms (same) ~15ms
Closed VS Code reads ~30ms, 30 processes ~5ms, 0 processes ~1ms
refreshSessionPreview (idle) ~10ms, 1-2 processes ~2ms ~0.5ms
Total per refresh ~490ms, ~530 processes ~120ms, 0 processes ~30ms
Improvement baseline ~4x faster ~16x faster

Recommendation: Node.js native reads first (4x, low effort), Rust later if needed (16x, high effort + napi-rs build toolchain).

Recommended Fix Order

  1. Replace shell exec with Node.js file reads — highest impact, moderate effort (~4x faster, eliminates ~530 process spawns)
  2. Batch React state updates — high impact, moderate effort
  3. Skip Highlighter when no search — low effort, minor impact
  4. Increase fs.watch debounce to 150ms — trivial change
  5. Increase closed scan cache TTL — trivial change
  6. Cancel async tasks on tab switch — moderate effort, medium impact
  7. Virtual list — high effort, high impact for large lists (future)
  8. Rust native module — high effort, highest impact (future, if Node.js reads insufficient)

Related

🤖 On behalf of @grimmerk — generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions