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
This commit is contained in:
commit
8700a171fb
4 changed files with 53 additions and 15 deletions
|
|
@ -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…
Reference in a new issue