anomalyco / anomalyco/opencode

web: sustained ~50% CPU and maxed GPU while live-rendering a streaming session

Open
#49,552 3 comments 0 reactions 1 assignee View on GitHub

@Brendonovich is already working on this.

Since Sep 17, 2026.

Dominant language
TypeScript
Stars
209k
Forks
27.5k
PR merge metrics
PR metrics pending

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 via opencode pair)
  • Install/channel: Arch Linux package (pacman), v2 release channel
  • Active plugins: none

Reproduction

  1. Start the background service (opencode service start) and open the web client in a browser using the credentials from opencode pair.
  2. Stream a long agent turn in the session that the web tab displays: any long response, amplified by large tool outputs (e.g. a cat of a multi-MB file).
  3. 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 against gt_max_freq_mhz).
  4. 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:

  • #48434 — web, closest mechanism: timeline row reconciliation (Effect deep equality) on every stream delta; fix PR #48435. This report additionally covers the observer→rAF re-measure and scroll-reconcile chains, the resulting GPU compositing load, and the idle baseline.
  • #31664 — v1 Desktop app (Electron, Windows): GPU load from continuous CSS animations while "working" — different client and root cause.
  • #43355 — Desktop renderer frozen by an escalating ResizeObserver loop — same fragile path; here it is sustained heavy load, no freeze.
  • #46341 — server-side sustained CPU/RSS and SQLite event-table bloat — different layer than client rendering.
  • #14114 — v1-era web UI SSE streaming bandwidth/CPU — closed 2026-07-04, not planned, unresolved.
  • #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 requestAnimationFrame work:
    • ResizeObserver → rAF row re-measure (resizeItem / height recalculation) on the virtualized list,
    • MutationObserver on 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 setInterval sites 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.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.