Chasing many-pane smoothness: a WebGL renderer pulled for safety, and the error boundary that should have been there all along
Running ~9 streaming Claude panes felt laggy, so I reached for the obvious lever — xterm's WebGL renderer. It compiled and the IPC-batching half landed cleanly, but a separate blank-window crash (opening the summary) exposed a deeper gap: the app had no React error boundary, so any single render throw whited out the entire window with zero diagnostics. I disabled WebGL (the riskier lever, prime suspect for a blank at high pane counts), shipped the safe wins, and added the boundary that turns a future blank into a readable, recoverable error.
AI 버전
The itch: 9 panes, not smooth
Dogfooding with ~9 streaming Claude panes, the terminals felt laggy — nowhere near the buttery feel of a native app. xterm.js ships a DOM renderer by default (fine for one pane, janky for many) and a GPU WebGL renderer addon. The plan was two levers:
- WebGL renderer — the big one for terminal paint performance.
- Augmentor IPC batching — the app was calling into the Rust backend on every chunk of PTY output to log a parsed event; ×9 verbose TUIs that's a flood. Batch them on a 250 ms throttle into one call.
What landed, and what got pulled
The IPC batching is a clean, low-risk win and stayed. WebGL compiled and ran — but two things made me pull it:
- The wrong addon trap: the stable
@xterm/addon-canvasonly supports xterm v5; we're on v6, where canvas is effectively deprecated in favour of WebGL. So canvas was off the table, and WebGL is the only maintained GPU path. - The blank-window scare: separately, opening the summary blanked the whole window. WebGL is a known blank-risk at high pane counts (each terminal takes a GPU context; webviews cap simultaneous contexts, and a lost context blanks the surface). Even though the blank turned out to be in a build without WebGL (so WebGL wasn't that particular cause — see the post-mortem), a blind GPU-renderer change you can't visually verify, at exactly the pane counts where it's riskiest, is not worth shipping over a flaky feature. Disabled it; kept the safe IPC batching.
The real lesson was orthogonal to performance
Chasing the blank surfaced the actual gap: there was no React error boundary anywhere. In React, an uncaught throw during render with no boundary unmounts the whole tree → a blank #root, with no error text, no stack, nothing. So any rare render glitch presented as a terrifying total whiteout.
That's the fix that mattered most:
- A top-level
ErrorBoundarywrapping the app, plus one around each modal (summary / log / graph), so a crash degrades to a small recoverable panel (message + stack + retry/reload) and the terminals survive. - The boundary is also a diagnostic: the next time something throws, it shows the real error instead of a blank — which is the only way to pin a cause that, by definition, was never captured before.
Shipped in this bundle
- ⚡ Augmentor IPC batching (250 ms throttle) — the surviving perf win.
- 🛡️ ErrorBoundary (app root + per-modal) — blank window → recoverable error.
- 🧹 Orphan tmux-session GC — the dedicated
-L dalkkakserver had leaked ~20 detached sessions from past app quits (the app never kills sessions on quit, by design, so Claude sessions survive a relaunch — but nothing GC'd the truly-unreferenced ones). Now reaped on prod mount, guarded to never touch attached or layout-referenced sessions. - 🆕 Log decision trail — the ⌘L conversation log now shows the options an AskUserQuestion offered and which one was picked.
- ❌ WebGL renderer — written, then disabled. The hook stays in the registry for a safer future opt-in (focused-pane-only, with context-loss fallback, now behind the error boundary).
Honest status
This bundle is mostly features + robustness, not the big perf win I set out for. The headline smoothness lever (GPU rendering) is parked until it can be done safely and verifiably. The next pass is the real one: bring back GPU rendering carefully, cut React re-renders across panes, and treat motion/animation as a first-class polish target — because "feels native" is mostly 60 fps compositing and restraint, not raw throughput.
리뷰 필요
내 시각이 아직 안 들어간 entry.