Repository navigation
Description
Summary
The v2 web client sustains a large fraction of a CPU core and keeps the GPU at its maximum clock for the entire duration of a streaming session, then drops to ~0 when the session is idle. The load is disproportionate for streaming text and scales with session content size; it appears to come from per-event timeline re-rendering without batching or throttling.
Environment
- opencode version: 2.0.5 (channel: latest; 2.0.6 does not change this code path — I audited the full v2.0.5...v2.0.6 diff before reporting)
- OS: Linux 7.2.6-arch2-1 (Arch Linux, x86_64), KDE Plasma on Wayland, hybrid laptop (Intel UHD iGPU + RTX 3050); the browser composites on the iGPU
- Web client: Firefox 156.0 (opencode web UI served locally by
opencode serve, paired viaopencode pair) - Install/channel: Arch Linux package (pacman), v2 release channel
- Active plugins: none
Reproduction
- Start the background service (
opencode service start) and open the web client in a browser using the credentials fromopencode pair. - Stream a long agent turn in the session that the web tab displays: any long response, amplified by large tool outputs (e.g. a
catof a multi-MB file). - While streaming, watch:
- the Firefox content process CPU (
ps -eo pid,pcpu,args | grep 'firefox -contentproc'), and - the iGPU: KDE GPU-load widget, or
cat /sys/class/drm/card1/gt_cur_freq_mhz(compare againstgt_max_freq_mhz).
- the Firefox content process CPU (
- Let the session go idle; observe the load fall back to baseline.
Consistently reproducible: the load tracks streaming activity 1:1 (high while any event for the displayed session is being rendered, ~0 when idle).
Expected Behavior
Rendering the live timeline while a session streams should cost on the order of a terminal streaming text: brief, batched work per chunk, with the GPU and CPU near idle between bursts.
Actual Behavior
While the displayed session streams (measured on 2.0.5, repeated runs):
| State | Firefox content CPU | iGPU (Alder Lake UHD, max 1300 MHz) |
|---|---|---|
| Web tab closed | ~0 | 300–600 MHz (idle) |
| Web tab open, session idle | ~0 | near-0 utilization |
| Web tab open, session streaming | ~35–50% of one core, nearly constant | pinned at 1300 MHz (max) |
The KDE GPU widget correspondingly shows the iGPU near 100% for the whole duration of the stream, even though no interaction with the page is happening.
Additional Context
Related issues:
- app: timeline row reconciliation uses Effect deep equality on every stream delta #48434 — web, closest mechanism: timeline row reconciliation (Effect deep equality) on every stream delta; fix PR refactor(app): avoid deep equality in timeline row reconciliation #48435. This report additionally covers the observer→rAF re-measure and scroll-reconcile chains, the resulting GPU compositing load, and the idle baseline.
- [BUG - Desktop] High iGPU load 50% or more during task execution. #31664 — v1 Desktop app (Electron, Windows): GPU load from continuous CSS animations while "working" — different client and root cause.
- [Desktop] UI freezes after agent turns finish — renderer stuck in ResizeObserver loop; only force-quit + relaunch recovers #43355 — Desktop renderer frozen by an escalating ResizeObserver loop — same fragile path; here it is sustained heavy load, no freeze.
- [Bug]: sustained high CPU and memory growth in long-running web sessions with large SQLite history #46341 — server-side sustained CPU/RSS and SQLite event-table bloat — different layer than client rendering.
- [Performance] Web UI consumes excessive bandwidth via SSE streaming (~25MB/min) #14114 — v1-era web UI SSE streaming bandwidth/CPU — closed 2026-07-04, not planned, unresolved.
- Web UI: high CPU during streaming, layout thrashing, and session switch lag #31548 — v1-era "Web UI: high CPU during streaming, layout thrashing" (per-delta Shiki WASM on main thread, dual store mutations, scroll-anchor rAF thrashing, non-virtualized diffs) — auto-closed 2026-08-09, not planned, unresolved.
Static analysis of the 2.0.5 web bundle (served assets) shows the mechanism:
- The web client subscribes to the server's global event stream (with stall-detection abort + reconnect/backoff).
- The session route renders the timeline as a virtualized list. Each streamed event mutates the timeline DOM, which re-triggers a chain of observers that schedule
requestAnimationFramework:ResizeObserver→ rAF row re-measure (resizeItem/ height recalculation) on the virtualized list,MutationObserveron the timeline container (childList/subtree/characterData) → rAF scroll/reveal reconciliation,- message-count change → rAF scroll-position restore,
- a history-preload rAF chain that keeps firing while the viewport is not full.
- I found no batching, coalescing, or idle-throttling of these re-render paths in the 2.0.5 bundle (all
setIntervalsites are legitimate: a 1s shell-output poller while a shell tool runs, a retry countdown; no unconditional idle loop exists — the load strictly correlates with incoming events).
Notes:
- The cost scales with content size: the measurements above were taken on a session containing several multi-MB tool outputs; even so, ~50% of a core + pinned-max GPU for a text stream looks well above the expected cost.
- 2.0.6 audit: the only session-timeline changes in v2.0.5...v2.0.6 are attachment-card display (
SessionUserAttachmentReference); no render-pipeline changes. - Workaround: closing the web tab (or navigating away from the active session) stops the load immediately.
Activity
github-actions commentedon Sep 17, 2026
This issue might be a duplicate of existing issues. Please check:
If these are separate from what you're reporting, please clarify how this differs. Otherwise, adding detail to one of the existing open issues may be more productive.
Idsa0 commentedon Sep 17, 2026
I compared each of the suggested duplicates:
Keeping this open as the symptom-level report for the v2 web client (GPU measurements + bundle analysis); happy to close it if you'd rather consolidate tracking under #48434.
Lightningxxl commentedon Sep 19, 2026
Adding a data point from Desktop (Electron) 1.18.31 on macOS that supports the per-event re-render hypothesis and separates it from the idle ResizeObserver loop (#38296):
Suggests the batching/throttling fix here and the ResizeObserver guard in #38296 address two distinct cost components; fixing only one leaves the other.
opencodejr commentedon Oct 28, 2026
Thanks for the detailed measurements. I couldn't reproduce this in our automated sandbox: it has no desktop browser and no GPU, so it can't show the GPU and compositing load you describe. A maintainer will need to check it with a real browser profile.
The closest existing report is #48434, which is still open. It found that timeline row reconciliation does a deep-equality comparison on every streamed delta, and that cost grows with session size. Its fix PR, #48435, was closed without being merged. That may be part of what you're seeing, but your report also covers the GPU load and the observer/
requestAnimationFramework, so this issue stays separate.A Firefox Profiler recording (https://profiler.firefox.com) taken while a session streams would help a maintainer narrow it down.
Idsa0 commentedon Oct 31, 2026
Update (v2.2.5) — residual isolated to always-on working-state animations; cost scales with display refresh rate
Re-tested on v2.2.5 (Firefox, Linux). After isolating the cost, the residual is a different, continuously-running cost than what #60489 addressed.
What it is / isn't. Two PRs shipped in the 2.2.3 release to address other slices of this symptom — #60489 (per-delta timeline row-equality; closes #48434) and #60481 (stops the
ResizeObserver looperrors while rows grow; explicitly references this issue). Both cut per-frame/streaming overhead, but neither changes the compositor frame rate, and neither touches this cost — the remaining working-state GPU load is animation-driven. Cleanest isolation, on the same build and same display rate: animations off → working-state GPU ~0 %; animations on → sustained (~40 % at 60 Hz, ~70 % at the 144 Hz baseline).Mechanism (v2.2.5 web bundle). The working/thinking state carries a set of always-on CSS animations. The dominant one is the status label rendered by
[data-component="text-shimmer"], which draws the label one animated span per character, each running an infinitetext-shimmer-sweepkeyframe animatingbackground-positionunderbackground-clip:text(will-change: background-position) — a paint property, forcing a repaint each frame. It's accompanied by other working-state indicators, e.g. the busy tab mark[data-titlebar-tab][data-status=busy] [data-mark=busy]rendering aloader-v2spinner (transform: rotate, compositor-cheap), plus pulse/caret opacity animations.Measured (Firefox Profiler, working state, no text streaming):
Same per-frame animation count at both rates; every compositor/paint metric scales linearly with display refresh rate (vsync-capped), and GPU utilization follows: ~40 % at 60 Hz → ~70 % at the native 144 Hz baseline (peaking while streaming, when the paint-heavy shimmer is active), reproducing the original high-GPU symptom at the native rate.
Recordings: 144 Hz — https://share.firefox.dev/4if8h1v · 60 Hz — https://share.firefox.dev/4zqm0Zq
Same persistent-animation GPU-burn class as #31664, scoped to named elements, and a real cost at any refresh rate (≈2.4× at 144 Hz vs 60 Hz).
Shipped mitigation:
prefers-reduced-motion/data-motion="reduce"setsanimation: noneon these elements (which is why disabling animations drops GPU to ~0 %).Suggested fixes:
transform/mask) instead of per-characterbackground-positionanimations.Keeping this open as the element-level report for the v2 web client.