From 3207310448cf0e2804dceaaa2302fa27ce213979 Mon Sep 17 00:00:00 2001 From: BoulderBadgeDad Date: Sun, 28 Jun 2026 16:22:10 -0700 Subject: [PATCH] reduce-effects: kill expensive GPU properties, stop freezing functional motion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "Reduce visual effects" was a sledgehammer: body.reduce-effects * forced animation:none + transition-duration:0s on every element. That froze every CSS loading spinner mid-rotation — including the dash-header worker-service spinners (musicbrainz / spotify / deezer / … .active .-spinner) — which read as BROKEN rather than "off". It also killed cheap hover feedback like the Quick Actions buttons. The actual lag (esp. Firefox, see the #935 sweep) is backdrop-filter / box-shadow / filter re-rasterizing every frame — NOT the animations themselves. Transform- and opacity-only motion (the spinners) composites for ~free. So: keep forcing the expensive properties to none (unchanged — that's the real fix), but drop the blanket animation/transition kills. !important author declarations outrank animation + transition declarations in the cascade, so any keyframe/transition that tries to set blur/shadow/filter is still neutralized even while it runs — the spinner spins, just without the glow. Net: functional spinners stay alive, Quick Actions hover (transform + border-colour) returns, box-shadow transitions are no-ops (shadow forced none), and the GPU-heavy rendering that caused the lag stays gone. The worker-orb CANVAS is unaffected (JS-gated separately) and stays off under reduce-effects, as intended. Static guard test pins the contract: the global rule must keep the expensive-property kills and must NOT reintroduce blanket animation:none / transition:0s. --- tests/test_reduce_effects_css.py | 39 ++++++++++++++++++++++++++++++++ webui/static/style.css | 17 ++++++++++---- 2 files changed, 51 insertions(+), 5 deletions(-) create mode 100644 tests/test_reduce_effects_css.py diff --git a/tests/test_reduce_effects_css.py b/tests/test_reduce_effects_css.py new file mode 100644 index 00000000..86efb48d --- /dev/null +++ b/tests/test_reduce_effects_css.py @@ -0,0 +1,39 @@ +"""Guard the reduce-visual-effects CSS contract. + +Performance mode must kill the GPU-EXPENSIVE properties (backdrop-filter / box-shadow +/ filter — the real lag, esp. on Firefox) but must NOT blanket-freeze animations: +`animation: none` on every element stuck the dash-header worker-service spinners +mid-rotation, which read as "broken" (Discord/Boulder). Pins the surgical rule so a +future edit can't silently re-freeze the functional spinners or kill cheap hover +feedback.""" + +import re +from pathlib import Path + +_STYLE = Path(__file__).resolve().parent.parent / "webui" / "static" / "style.css" + + +def _reduce_effects_global_body() -> str: + css = _STYLE.read_text(encoding="utf-8") + m = re.search(r"body\.reduce-effects \*,.*?\{([^}]*)\}", css, re.DOTALL) + assert m, "global 'body.reduce-effects *' rule not found" + return m.group(1) + + +def test_reduce_effects_still_kills_expensive_gpu_properties(): + body = _reduce_effects_global_body() + for prop in ("backdrop-filter: none", "box-shadow: none", "filter: none"): + assert prop in body, f"reduce-effects must still force {prop} (the real lag source)" + + +def test_reduce_effects_does_not_blanket_freeze_animations(): + body = _reduce_effects_global_body() + # `animation: none` on * froze the dash-header worker-service spinners mid-spin; + # cheap transform/opacity motion must survive so a spinner still reads as "working". + assert "animation: none" not in body, ( + "blanket 'animation: none' re-freezes the worker spinners — keep motion alive, " + "the expensive properties are already neutralized above" + ) + assert "transition-duration: 0s" not in body, ( + "blanket 'transition-duration: 0s' kills the cheap Quick Actions hover feedback" + ) diff --git a/webui/static/style.css b/webui/static/style.css index fabefe73..865b365f 100644 --- a/webui/static/style.css +++ b/webui/static/style.css @@ -60380,14 +60380,21 @@ tr:hover .enhanced-track-actions-group { opacity: 1; } } .blacklist-entry-remove:hover { background: rgba(239, 83, 80, 0.12); color: #ef5350; } -/* ── Reduce Visual Effects ── Full performance mode: disables GPU-heavy - properties and halts decorative animation globally. */ +/* ── Reduce Visual Effects ── Performance mode. Kills the GPU-EXPENSIVE + properties only — the actual lag (esp. Firefox) is backdrop-filter / box-shadow / + filter re-rasterizing every frame. Because !important author declarations outrank + animation + transition declarations in the cascade, any keyframe/transition that + tries to set these is forced to none even while it runs — so the expensive part is + neutralized without freezing the motion itself. + + We deliberately do NOT blanket `animation: none` / `transition-duration: 0s`: + that froze every functional loading spinner (the dash-header worker-service + spinners read as BROKEN, stuck mid-rotation) and killed cheap transform/colour + feedback (e.g. the Quick Actions hover). Transform- and opacity-only motion is + composited for ~free, so it stays — a spinner that spins, just without glow. */ body.reduce-effects *, body.reduce-effects *::before, body.reduce-effects *::after { - animation: none !important; - transition-duration: 0s !important; - transition-delay: 0s !important; backdrop-filter: none !important; -webkit-backdrop-filter: none !important; box-shadow: none !important;