Merge pull request #793 from nick2000713/perf/scroll-render-and-pm-compat

perf(webui): fix scroll-container raster jank + make the UI usable with password-manager extensions
pull/795/head
BoulderBadgeDad 3 weeks ago committed by GitHub
commit 8700a171fb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -65,6 +65,14 @@ function _handleRateMonitorUpdate(data) {
const grid = document.getElementById('rate-monitor-grid');
if (!grid) return;
// Skip DOM writes while the equalizer is off-screen (you're on another page).
// All pages stay mounted, so updating a hidden grid still fires every
// MutationObserver on the document — including password-manager extensions
// that re-scan the WHOLE DOM on each mutation — for zero visible benefit.
// offsetParent is null when an ancestor is display:none. The next update that
// arrives while the dashboard is visible renders normally.
if (grid.offsetParent === null) return;
// The dashboard rate monitor uses the equalizer-bar visual — a
// vertical-bar VU-meter row that fits any service count without
// an orphan grid cell. Detail page / mobile breakpoints keep the

@ -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) => {

@ -143,6 +143,25 @@ function handleMetadataSourceChange(event) {
}
let _settingsInitialized = false;
// Tell password-manager extensions (Bitwarden / 1Password / LastPass) to ignore
// this app's credential inputs. The settings page is full of API-key / token /
// secret fields; password managers treat them as login forms and re-scan the
// whole (large, constantly-mutating) DOM on every change, which can peg the main
// thread for seconds. These attributes make them skip the fields entirely.
function _markCredentialFieldsNoAutofill(root) {
const scope = root || document;
scope.querySelectorAll('input, textarea').forEach((el) => {
if (el.dataset.bwignore !== undefined) return; // already tagged
el.setAttribute('data-bwignore', ''); // Bitwarden
el.setAttribute('data-1p-ignore', ''); // 1Password
el.setAttribute('data-lpignore', 'true'); // LastPass
if (!el.getAttribute('autocomplete')) el.setAttribute('autocomplete', 'off');
});
}
// Run once on load (inputs exist from page load — all pages are mounted).
if (document.readyState !== 'loading') _markCredentialFieldsNoAutofill();
else document.addEventListener('DOMContentLoaded', () => _markCredentialFieldsNoAutofill());
function initializeSettings() {
// This function is called when the settings page is loaded.
// It attaches event listeners to all interactive elements on the page.
@ -151,6 +170,9 @@ function initializeSettings() {
if (_settingsInitialized) return;
_settingsInitialized = true;
// Re-tag in case any inputs were added dynamically since page load.
_markCredentialFieldsNoAutofill(document.getElementById('settings-page'));
// Accent color listeners (live preview + custom picker toggle)
initAccentColorListeners();

@ -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;
}

Loading…
Cancel
Save