Problem
The Research pane's animated accent ring is a perpetual per-frame paint producer.
static/js/research/panel.js:_ensureOrbit() runs a requestAnimationFrame loop that rewrites the --research-orbit-angle CSS variable every frame while the panel is open — by design even when idle with no research job running (ambient mode, _orbitSpeedDegPerSec = running > 0 ? 60 : 22). The loop only stops when the pane is removed.
That variable feeds static/style.css .research-pane::after:
.research-pane::after {
position: absolute; inset: 0; /* full pane area */
background: conic-gradient(from var(--research-orbit-angle), …);
mask: …; mask-composite: exclude; /* dual-mask to a 2px ring */
}
Every frame the browser recomputes a conic-gradient over the entire pane box and re-applies an exclude mask-composite, then re-rasterizes — 60 fps, forever. This is the worst-case 'paint, not compositor' pattern (large area + mask), and it is invisible work when idle (no job, user not looking).
Impact
Discovered via CDP (tooling/mem-probe.py raf flagged _ensureOrbit <- _syncResearchRail). On hardware GPU it's a steady background cost; under software rendering (llvmpipe) it is catastrophic. Same family as the spinner (#107) and brain-panel (#108) perpetual-animation fixes, but worse (full-area + mask).
Proposed fix
- Freeze the orbit when no job is running (the ambient idle spin is pure decoration burning frames) — leave a static ring; only drive the rAF while
running > 0. Eliminates the idle case entirely.
- When it does run, prefer a compositor approach (static gradient +
transform: rotate()) or throttle below 60 fps. Note: a traveling arc around a rounded-rect can't be fully compositor-free, so freezing-when-idle is the high-value win.
- Honor
prefers-reduced-motion (already partially done — the ::after has a reduce rule, but the JS rAF ignores it; gate the JS too).
Evidence: this session's CPU investigation; static/js/research/panel.js:160-183, static/style.css:35927.
Problem
The Research pane's animated accent ring is a perpetual per-frame paint producer.
static/js/research/panel.js:_ensureOrbit()runs a requestAnimationFrame loop that rewrites the--research-orbit-angleCSS variable every frame while the panel is open — by design even when idle with no research job running (ambient mode,_orbitSpeedDegPerSec = running > 0 ? 60 : 22). The loop only stops when the pane is removed.That variable feeds
static/style.css.research-pane::after:Every frame the browser recomputes a conic-gradient over the entire pane box and re-applies an exclude mask-composite, then re-rasterizes — 60 fps, forever. This is the worst-case 'paint, not compositor' pattern (large area + mask), and it is invisible work when idle (no job, user not looking).
Impact
Discovered via CDP (
tooling/mem-probe.py rafflagged_ensureOrbit <- _syncResearchRail). On hardware GPU it's a steady background cost; under software rendering (llvmpipe) it is catastrophic. Same family as the spinner (#107) and brain-panel (#108) perpetual-animation fixes, but worse (full-area + mask).Proposed fix
running > 0. Eliminates the idle case entirely.transform: rotate()) or throttle below 60 fps. Note: a traveling arc around a rounded-rect can't be fully compositor-free, so freezing-when-idle is the high-value win.prefers-reduced-motion(already partially done — the ::after has a reduce rule, but the JS rAF ignores it; gate the JS too).Evidence: this session's CPU investigation;
static/js/research/panel.js:160-183,static/style.css:35927.