From 240dce0c1b8adb5cf4f6f7a89476987e900c54b7 Mon Sep 17 00:00:00 2001 From: BoulderBadgeDad Date: Sat, 27 Jun 2026 13:07:03 -0700 Subject: [PATCH] worker-orbs: Firefox compositor keep-alive fixes post-hover 1fps slowdown (#935) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit removing the always-on dash-card blob animation (for the Chrome GPU win) incidentally let Firefox start throttling the worker-orb canvas's compositing to ~1fps after a header hover re-layerizes the dashboard — Chrome never throttles it. re-add the 'keep the compositor warm' effect cheaply: a 2px, ~invisible element running an infinite transform-only animation (zero paint). gated behind CSS.supports('-moz-appearance') so it's Firefox-only; Chrome never gets it. confirmed fix in Firefox/Zen. --- webui/static/worker-orbs.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/webui/static/worker-orbs.js b/webui/static/worker-orbs.js index 199b1eeb..6135edcf 100644 --- a/webui/static/worker-orbs.js +++ b/webui/static/worker-orbs.js @@ -97,6 +97,24 @@ dashboardHeader.appendChild(canvas); ctx = canvas.getContext('2d'); + // #935: on Firefox, a header hover re-layerizes the dashboard and the engine then + // throttles THIS canvas's compositing to ~1fps (Chrome never does this). The old + // always-on dash-card blob animation incidentally kept the compositor on the fast + // path; once that was removed for the Chrome GPU win, the throttle showed up. Re-add + // that "keep-alive" cheaply: a 2px, ~invisible element running an infinite transform- + // only animation (compositor work, zero paint). Firefox-only via the same feature + // gate the CSS uses — Chrome doesn't throttle and never gets this. + if (window.CSS && CSS.supports && CSS.supports('-moz-appearance', 'none')) { + const keepAlive = document.createElement('div'); + keepAlive.className = 'worker-orb-ff-keepalive'; + keepAlive.setAttribute('aria-hidden', 'true'); + keepAlive.style.cssText = 'position:absolute;top:0;left:0;width:2px;height:2px;opacity:0.01;pointer-events:none;z-index:0;'; + keepAlive.animate( + [{ transform: 'translateX(0)' }, { transform: 'translateX(1px)' }], + { duration: 1500, iterations: Infinity, direction: 'alternate' }); + dashboardHeader.appendChild(keepAlive); + } + WORKER_DEFS.forEach((def, i) => { const el = headerActions.querySelector(def.container); if (!el) return;