From 0fe0a4c45b6fa5c953bce8e370c4c71a26b2869b Mon Sep 17 00:00:00 2001 From: dev Date: Thu, 4 Jun 2026 21:53:10 +0200 Subject: [PATCH] perf(webui): make the UI usable with password-manager extensions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Password managers (Bitwarden/1Password/LastPass) treat this app's many API-key/ token/secret fields as login forms and re-scan the whole, constantly-mutating DOM on every change — pegging the main thread for seconds and making hover/click/ scroll feel laggy. Two mitigations (measured to make the app usable with the extension enabled): - Tag all inputs with data-bwignore / data-1p-ignore / data-lpignore so the managers skip them (no autofill detection work). - Rate-monitor equalizer: skip DOM writes while it's off-screen (offsetParent null). All pages stay mounted, so updating the hidden grid still triggered the managers' MutationObserver on every backend rate-monitor event for no benefit. Co-Authored-By: Claude Opus 4.8 --- webui/static/api-monitor.js | 8 ++++++++ webui/static/settings.js | 22 ++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/webui/static/api-monitor.js b/webui/static/api-monitor.js index 9e1c8e88..1b4215b1 100644 --- a/webui/static/api-monitor.js +++ b/webui/static/api-monitor.js @@ -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 diff --git a/webui/static/settings.js b/webui/static/settings.js index 83472307..6c402445 100644 --- a/webui/static/settings.js +++ b/webui/static/settings.js @@ -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();