From 89fe7703fa850afe8a0167179ff789f5c5aff1f9 Mon Sep 17 00:00:00 2001 From: dev Date: Thu, 4 Jun 2026 20:35:21 +0200 Subject: [PATCH] perf(webui): flatten scroll-container background + scope explorer wheel listener Two measured, universally-beneficial fixes (kept after determining the rest of the earlier perf work was chasing a Bitwarden extension that pegged the main thread, not real app bugs): - .main-content had a linear-gradient background. A gradient on the scroll container is re-rastered across the whole scrolled area every scroll frame (the compositor can't translate a cached tile): ~25% dropped frames -> <1% once flattened to a solid color (visually identical, was rgb 10->15->11). - The explorer wheel-zoom listener was a non-passive listener on `document`, which disables compositor (async) scrolling app-wide so every wheel/trackpad scroll runs through the main thread. Scoped it to the explorer viewport. Co-Authored-By: Claude Opus 4.8 --- webui/static/pages-extra.js | 26 ++++++++++++++++---------- webui/static/style.css | 12 +++++++----- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/webui/static/pages-extra.js b/webui/static/pages-extra.js index 67b016ae..d9841973 100644 --- a/webui/static/pages-extra.js +++ b/webui/static/pages-extra.js @@ -1066,18 +1066,24 @@ function explorerFitToView() { }); } -// Scroll wheel zoom (no modifier needed inside viewport) -document.addEventListener('wheel', (e) => { +// Scroll wheel zoom (no modifier needed inside viewport). +// IMPORTANT: attach to the viewport element, NOT document. A non-passive wheel +// listener on document disables the browser's compositor (async) scrolling for +// the ENTIRE app — every wheel/trackpad scroll then runs through the main thread. +// Scoping it to the viewport keeps zoom working while the rest of the app keeps +// smooth compositor scrolling. +(function attachExplorerWheelZoom() { const viewport = document.getElementById('explorer-viewport'); - if (!viewport || !viewport.contains(e.target)) return; - // Check if we're on the explorer page - const page = document.getElementById('playlist-explorer-page'); - if (!page || !page.classList.contains('active')) return; + if (!viewport) return; + viewport.addEventListener('wheel', (e) => { + const page = document.getElementById('playlist-explorer-page'); + if (!page || !page.classList.contains('active')) return; - e.preventDefault(); - const step = e.deltaY > 0 ? -0.08 : 0.08; - explorerZoom(step); -}, { passive: false }); + e.preventDefault(); + const step = e.deltaY > 0 ? -0.08 : 0.08; + explorerZoom(step); + }, { passive: false }); +})(); // Middle-click / right-click drag to pan document.addEventListener('mousedown', (e) => { diff --git a/webui/static/style.css b/webui/static/style.css index 8c521124..893fbaa2 100644 --- a/webui/static/style.css +++ b/webui/static/style.css @@ -2970,11 +2970,13 @@ body.helper-mode-active #dashboard-activity-feed:hover { .main-content { flex: 1; - /* Opaque dark background (GPU-optimized: no backdrop-filter needed on solid dark body) */ - background: linear-gradient(135deg, - rgba(10, 10, 10, 1) 0%, - rgba(15, 15, 15, 1) 50%, - rgba(11, 11, 11, 1) 100%); + /* Flat solid background — NOT a gradient. A gradient on the scroll container + has to be re-rastered across the whole scrolled area on every scroll frame + (the compositor can't just translate a cached tile), which caused the + dominant scroll jank (~25% dropped frames -> <1% once flattened, measured). + A solid color is drawn as a single compositor quad, no per-frame raster. + The gradient was rgb 10->15->11, visually indistinguishable from this. */ + background: rgb(12, 12, 12); overflow: auto; position: relative; }