Description
The diff pipeline (both VCS diff and Session diff) causes severe UI jank on the desktop app. Disabling both systems completely eliminates the lag — confirming the diff subsystem as the root cause.
This affects two independent diff systems:
- VCS diff (
/vcs/diff endpoint) — git working tree / branch diff
- Session diff (
SessionSummary) — AI-generated file change diffs per session
Root Cause Analysis
The core design issue is using full-context unified patches as the universal transport format between backend and frontend, which creates triple redundant work:
- Backend generates full-file patches with
formatPatch(structuredPatch(..., { context: Number.MAX_SAFE_INTEGER })) — expensive git ops + string processing
- Frontend parses the patch back into before/after content via
parsePatch() in session-diff.ts
- Frontend feeds reconstructed content to
@pierre/diffs parseDiffFromFile() which re-diffs it — synchronously on the main thread
Bottleneck Ranking (by perceived lag contribution)
| Rank |
Bottleneck |
Location |
Impact |
| #1 |
SessionReview.items() eagerly runs normalize() → parseDiffFromFile() for ALL files synchronously on main thread |
packages/ui/src/components/session-review.tsx + session-diff.ts |
UI freeze |
| #2 |
Backend generates Number.MAX_SAFE_INTEGER context patches — oversized payloads |
packages/opencode/src/project/vcs.ts + snapshot/index.ts |
Amplifies #1 |
| #3 |
Whole-array reconcile() on every SSE session.diff event + 7 createMemo per file + getBoundingClientRect() per file per scroll frame |
event-reducer.ts + session-review.tsx |
Sustained frame drops |
| #4 |
Snapshot.diffFull locked with semaphore(1), git show fallback concurrency: 2 |
snapshot/index.ts |
Throughput bottleneck |
Suggested Fix Strategy
1. Split diff API into summary / detail
- Summary (list view): return only
{ file, status, additions, deletions } — no patches
- Detail (per-file, on expand): return
{ before, after, status } for a single file
- Both VCS and Session diff share this contract
2. Lazy normalize in SessionReview
- Stop eagerly calling
normalize() for all files in items()
- Only compute
FileDiffMetadata when a file is expanded && visible
3. Move parseDiffFromFile() to web worker
- Current worker pool (size=2) only handles Shiki highlighting
- Diff parsing should also be offloaded from the main thread
4. State granularity + debounce
- Split
session_diff store into summaryBySession / detailByKey
- Add 150–250ms debounce to SSE diff event handling
- Replace
rAF + getBoundingClientRect() scroll tracking with IntersectionObserver
5. Backend: cache + reduce redundancy
- Cache
Snapshot.diffFull results by (from, to) snapshot pair (immutable)
- Separate read lock from write lock on snapshot semaphore
- Combine the two sequential
git diff calls (--name-status + --numstat) where possible
6. Remove Number.MAX_SAFE_INTEGER from interactive path
- Warning: cannot simply change to
-U3 without updating the client — session-diff.ts currently depends on full-context patch to reconstruct before/after content
- For export/share use cases, generate full patches via a separate background path
Related Issues
Environment
- OpenCode version: 1.4.x
- OS: macOS (Apple Silicon)
- Client: Desktop app
- Repo size: medium-large with git submodules
Description
The diff pipeline (both VCS diff and Session diff) causes severe UI jank on the desktop app. Disabling both systems completely eliminates the lag — confirming the diff subsystem as the root cause.
This affects two independent diff systems:
/vcs/diffendpoint) — git working tree / branch diffSessionSummary) — AI-generated file change diffs per sessionRoot Cause Analysis
The core design issue is using full-context unified patches as the universal transport format between backend and frontend, which creates triple redundant work:
formatPatch(structuredPatch(..., { context: Number.MAX_SAFE_INTEGER }))— expensive git ops + string processingparsePatch()insession-diff.ts@pierre/diffsparseDiffFromFile()which re-diffs it — synchronously on the main threadBottleneck Ranking (by perceived lag contribution)
SessionReview.items()eagerly runsnormalize()→parseDiffFromFile()for ALL files synchronously on main threadpackages/ui/src/components/session-review.tsx+session-diff.tsNumber.MAX_SAFE_INTEGERcontext patches — oversized payloadspackages/opencode/src/project/vcs.ts+snapshot/index.tsreconcile()on every SSEsession.diffevent + 7createMemoper file +getBoundingClientRect()per file per scroll frameevent-reducer.ts+session-review.tsxSnapshot.diffFulllocked with semaphore(1), git show fallback concurrency: 2snapshot/index.tsSuggested Fix Strategy
1. Split diff API into summary / detail
{ file, status, additions, deletions }— no patches{ before, after, status }for a single file2. Lazy normalize in SessionReview
normalize()for all files initems()FileDiffMetadatawhen a file isexpanded && visible3. Move
parseDiffFromFile()to web worker4. State granularity + debounce
session_diffstore intosummaryBySession/detailByKeyrAF + getBoundingClientRect()scroll tracking withIntersectionObserver5. Backend: cache + reduce redundancy
Snapshot.diffFullresults by(from, to)snapshot pair (immutable)git diffcalls (--name-status+--numstat) where possible6. Remove
Number.MAX_SAFE_INTEGERfrom interactive path-U3without updating the client —session-diff.tscurrently depends on full-context patch to reconstruct before/after contentRelated Issues
Environment