perf(webui): make the UI usable with password-manager extensions

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 <noreply@anthropic.com>
This commit is contained in:
dev 2026-06-04 21:53:10 +02:00
parent 89fe7703fa
commit 0fe0a4c45b
2 changed files with 30 additions and 0 deletions

View file

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

View file

@ -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();