Performance fixes for UI

This commit is contained in:
2025-08-10 11:07:20 +12:00
parent bd86ffc423
commit 5a5cbc204f
3 changed files with 102 additions and 114 deletions
+51 -61
View File
@@ -1,4 +1,7 @@
const ipcRenderer = window.electronAPI; const ipcRenderer = window.electronAPI;
// Lightweight debug logger (toggleable)
const DEBUG = false;
const debug = (...args) => { if (DEBUG) console.log(...args); };
// Site history management using localStorage // Site history management using localStorage
function getSiteHistory() { function getSiteHistory() {
@@ -35,6 +38,7 @@ const webviewsEl = document.getElementById('webviews');
const menuPopup = document.getElementById('menu-popup'); const menuPopup = document.getElementById('menu-popup');
const contextMenu = document.getElementById('context-menu'); const contextMenu = document.getElementById('context-menu');
const menuItems = contextMenu ? contextMenu.querySelectorAll('li') : []; const menuItems = contextMenu ? contextMenu.querySelectorAll('li') : [];
let lastContextPos = { x: 0, y: 0 };
// Select all text on focus and prevent mouseup from deselecting // Select all text on focus and prevent mouseup from deselecting
urlBox.addEventListener('focus', () => { urlBox.addEventListener('focus', () => {
@@ -64,6 +68,19 @@ function scheduleRenderTabs() {
}); });
} }
// Debounce nav button updates to reduce layout work
let navButtonsPending = false;
let backBtnCached = null;
let fwdBtnCached = null;
function scheduleUpdateNavButtons() {
if (navButtonsPending) return;
navButtonsPending = true;
requestAnimationFrame(() => {
navButtonsPending = false;
try { updateNavButtons(); } catch {}
});
}
// Derive a stable, safe label for a tab without throwing on non-URLs // Derive a stable, safe label for a tab without throwing on non-URLs
function getTabLabel(tab) { function getTabLabel(tab) {
if (tab.title && tab.title !== 'New Tab') return tab.title; if (tab.title && tab.title !== 'New Tab') return tab.title;
@@ -105,13 +122,13 @@ loadBookmarks();
// Listen for site history updates from main process // Listen for site history updates from main process
ipcRenderer.on('record-site-history', (event, url) => { ipcRenderer.on('record-site-history', (event, url) => {
console.log('[DEBUG] Received site history update:', url); debug('[DEBUG] Received site history update:', url);
addToSiteHistory(url); addToSiteHistory(url);
}); });
function createTab(inputUrl) { function createTab(inputUrl) {
inputUrl = inputUrl || 'browser://home'; inputUrl = inputUrl || 'browser://home';
console.log('[DEBUG] createTab() inputUrl =', inputUrl); debug('[DEBUG] createTab() inputUrl =', inputUrl);
const id = crypto.randomUUID(); const id = crypto.randomUUID();
// Handle home page specially // Handle home page specially
@@ -139,7 +156,7 @@ function createTab(inputUrl) {
// For all other URLs, use webview // For all other URLs, use webview
const resolvedUrl = resolveInternalUrl(inputUrl); const resolvedUrl = resolveInternalUrl(inputUrl);
console.log('[DEBUG] createTab() resolvedUrl =', resolvedUrl); debug('[DEBUG] createTab() resolvedUrl =', resolvedUrl);
const webview = document.createElement('webview'); const webview = document.createElement('webview');
// give the webview an id and set its source and attributes so it actually loads and can be managed // give the webview an id and set its source and attributes so it actually loads and can be managed
@@ -173,37 +190,17 @@ function createTab(inputUrl) {
// Consolidated navigation recording - only use did-navigate to avoid duplicates // Consolidated navigation recording - only use did-navigate to avoid duplicates
webview.addEventListener('did-navigate', e => { webview.addEventListener('did-navigate', e => {
handleNavigation(id, e.url); handleNavigation(id, e.url);
// Record ALL HTTP navigations if (e.url.startsWith('http')) debug('[DEBUG] Recording navigation to:', e.url);
if (e.url.startsWith('http')) {
console.log('[DEBUG] Recording navigation to:', e.url);
addToSiteHistory(e.url);
// Also save to file for cross-context sharing
ipcRenderer.invoke('save-site-history-entry', e.url).catch(err =>
console.error('Failed to save to file:', err)
);
}
}); });
webview.addEventListener('did-navigate-in-page', e => { webview.addEventListener('did-navigate-in-page', e => {
handleNavigation(id, e.url); handleNavigation(id, e.url);
// Record in-page navigations too if (e.url.startsWith('http')) debug('[DEBUG] Recording in-page navigation to:', e.url);
if (e.url.startsWith('http')) {
console.log('[DEBUG] Recording in-page navigation to:', e.url);
addToSiteHistory(e.url);
ipcRenderer.invoke('save-site-history-entry', e.url).catch(err =>
console.error('Failed to save to file:', err)
);
}
}); });
// Also capture when pages finish loading // After load, just refresh nav buttons to avoid jank
webview.addEventListener('did-finish-load', () => { webview.addEventListener('did-finish-load', () => {
const currentUrl = webview.getURL(); scheduleUpdateNavButtons();
if (currentUrl.startsWith('http') && !currentUrl.includes('browser://')) {
console.log('[DEBUG] Webview did-finish-load, recording:', currentUrl);
addToSiteHistory(currentUrl);
ipcRenderer.invoke('save-site-history-entry', currentUrl);
}
}); });
// catch any target="_blank" or window.open() calls and open them as new tabs // catch any target="_blank" or window.open() calls and open them as new tabs
@@ -310,7 +307,7 @@ function navigate() {
webview.src = resolved; webview.src = resolved;
scheduleRenderTabs(); scheduleRenderTabs();
updateNavButtons(); scheduleUpdateNavButtons();
} }
function convertHomeTabToWebview(tabId, inputUrl, resolvedUrl) { function convertHomeTabToWebview(tabId, inputUrl, resolvedUrl) {
@@ -340,30 +337,12 @@ function convertHomeTabToWebview(tabId, inputUrl, resolvedUrl) {
webview.addEventListener('did-navigate', e => { webview.addEventListener('did-navigate', e => {
handleNavigation(tabId, e.url); handleNavigation(tabId, e.url);
if (e.url.startsWith('http')) {
addToSiteHistory(e.url);
ipcRenderer.invoke('save-site-history-entry', e.url).catch(err =>
console.error('Failed to save to file:', err)
);
}
}); });
webview.addEventListener('did-navigate-in-page', e => { webview.addEventListener('did-navigate-in-page', e => {
handleNavigation(tabId, e.url); handleNavigation(tabId, e.url);
if (e.url.startsWith('http')) {
addToSiteHistory(e.url);
ipcRenderer.invoke('save-site-history-entry', e.url).catch(err =>
console.error('Failed to save to file:', err)
);
}
}); });
webview.addEventListener('did-finish-load', () => { webview.addEventListener('did-finish-load', () => {
const currentUrl = webview.getURL(); scheduleUpdateNavButtons();
if (currentUrl.startsWith('http') && !currentUrl.includes('browser://')) {
addToSiteHistory(currentUrl);
ipcRenderer.invoke('save-site-history-entry', currentUrl);
}
}); });
webview.addEventListener('new-window', e => { webview.addEventListener('new-window', e => {
@@ -393,7 +372,7 @@ function convertHomeTabToWebview(tabId, inputUrl, resolvedUrl) {
if (homeContainer) homeContainer.classList.remove('active'); if (homeContainer) homeContainer.classList.remove('active');
webview.classList.add('active'); webview.classList.add('active');
updateNavButtons(); scheduleUpdateNavButtons();
// Activate converted webview tab and update UI // Activate converted webview tab and update UI
setActiveTab(tabId); setActiveTab(tabId);
scheduleRenderTabs(); scheduleRenderTabs();
@@ -403,7 +382,7 @@ function handleNavigation(tabId, newUrl) {
const tab = tabs.find(t => t.id === tabId); const tab = tabs.find(t => t.id === tabId);
if (!tab) return; if (!tab) return;
console.log('[DEBUG] handleNavigation called with:', newUrl); debug('[DEBUG] handleNavigation called with:', newUrl);
// --- record every real navigation into history --- // --- record every real navigation into history ---
if (tab.history[tab.historyIndex] !== newUrl) { if (tab.history[tab.historyIndex] !== newUrl) {
@@ -418,7 +397,7 @@ function handleNavigation(tabId, newUrl) {
!newUrl.startsWith('file://') && !newUrl.startsWith('file://') &&
!newUrl.includes('browser://') && !newUrl.includes('browser://') &&
newUrl.startsWith('http')) { newUrl.startsWith('http')) {
console.log('[DEBUG] Adding to site history:', newUrl); debug('[DEBUG] Adding to site history:', newUrl);
addToSiteHistory(newUrl); addToSiteHistory(newUrl);
// Also send to main process for file storage // Also send to main process for file storage
ipcRenderer.invoke('save-site-history-entry', newUrl); ipcRenderer.invoke('save-site-history-entry', newUrl);
@@ -440,7 +419,7 @@ function handleNavigation(tabId, newUrl) {
} }
scheduleRenderTabs(); scheduleRenderTabs();
updateNavButtons(); scheduleUpdateNavButtons();
} }
@@ -620,18 +599,19 @@ function goForward() {
function updateNavButtons() { function updateNavButtons() {
const webview = document.getElementById(`tab-${activeTabId}`); const webview = document.getElementById(`tab-${activeTabId}`);
const backBtn = document.querySelector('.nav-left button:nth-child(1)'); if (!backBtnCached || !fwdBtnCached) {
const forwardBtn = document.querySelector('.nav-left button:nth-child(2)'); backBtnCached = document.querySelector('.nav-left button:nth-child(1)');
fwdBtnCached = document.querySelector('.nav-left button:nth-child(2)');
backBtn.disabled = !webview || !webview.canGoBack(); }
forwardBtn.disabled = !webview || !webview.canGoForward(); if (backBtnCached) backBtnCached.disabled = !webview || !webview.canGoBack();
if (fwdBtnCached) fwdBtnCached.disabled = !webview || !webview.canGoForward();
} }
function reload() { function reload() {
const webview = document.getElementById(`tab-${activeTabId}`); const webview = document.getElementById(`tab-${activeTabId}`);
if (webview) { if (webview) {
webview.reload(); webview.reload();
updateNavButtons(); // keep back/forward buttons in sync after a reload scheduleUpdateNavButtons(); // keep back/forward buttons in sync after a reload
} }
} }
@@ -711,6 +691,13 @@ window.addEventListener('DOMContentLoaded', () => {
const forwardBtn = document.querySelector('.nav-left button:nth-child(2)'); const forwardBtn = document.querySelector('.nav-left button:nth-child(2)');
backBtn.addEventListener('click', goBack); backBtn.addEventListener('click', goBack);
forwardBtn.addEventListener('click', goForward); forwardBtn.addEventListener('click', goForward);
// cache for faster updates
backBtnCached = backBtn;
fwdBtnCached = forwardBtn;
// settings button
const settingsBtn = document.getElementById('open-settings-btn');
if (settingsBtn) settingsBtn.addEventListener('click', openSettings);
// window control bindings // window control bindings
const minBtn = document.getElementById('min-btn'); const minBtn = document.getElementById('min-btn');
@@ -782,6 +769,7 @@ try {
// 4) unify context-menu wiring // 4) unify context-menu wiring
function showContextMenu(x,y) { function showContextMenu(x,y) {
if (!contextMenu) return; if (!contextMenu) return;
lastContextPos = { x, y };
contextMenu.style.top = `${y}px`; contextMenu.style.top = `${y}px`;
contextMenu.style.left = `${x}px`; contextMenu.style.left = `${x}px`;
contextMenu.classList.add('visible'); contextMenu.classList.add('visible');
@@ -793,7 +781,8 @@ document.addEventListener('contextmenu', e => {
} }
}); });
document.addEventListener('click', ()=> contextMenu && contextMenu.classList.remove('visible')); document.addEventListener('click', ()=> contextMenu && contextMenu.classList.remove('visible'));
menuItems.forEach(item => item.addEventListener('click', async evt => { if (remote && fs) {
menuItems.forEach(item => item.addEventListener('click', async evt => {
const action = item.dataset.action; const action = item.dataset.action;
const win = remote.getCurrentWindow(); const win = remote.getCurrentWindow();
@@ -819,12 +808,13 @@ menuItems.forEach(item => item.addEventListener('click', async evt => {
break; break;
} }
case 'inspect-accessibility': case 'inspect-accessibility':
win.webContents.inspectAccessibilityNode(e.clientX, e.clientY); win.webContents.inspectAccessibilityNode(lastContextPos.x, lastContextPos.y);
break; break;
case 'inspect-element': case 'inspect-element':
win.webContents.inspectElement(e.clientX, e.clientY); win.webContents.inspectElement(lastContextPos.x, lastContextPos.y);
break; break;
} }
contextMenu.classList.remove('visible'); contextMenu.classList.remove('visible');
})); }));
}
+1 -13
View File
@@ -74,10 +74,8 @@ window.addEventListener('DOMContentLoaded', () => {
// Tabs: simple controller // Tabs: simple controller
function activateTab(tabName) { function activateTab(tabName) {
console.log('[TABS] Activating tab:', tabName);
const links = document.querySelectorAll('.tab-link'); const links = document.querySelectorAll('.tab-link');
const panels = document.querySelectorAll('.tab-panel'); const panels = document.querySelectorAll('.tab-panel');
console.log('[TABS] Found', links.length, 'tab links and', panels.length, 'panels');
links.forEach(l => { links.forEach(l => {
const isActive = l.dataset.tab === tabName; const isActive = l.dataset.tab === tabName;
@@ -89,21 +87,17 @@ function activateTab(tabName) {
const isActive = p.id === `panel-${tabName}`; const isActive = p.id === `panel-${tabName}`;
p.classList.toggle('active', isActive); p.classList.toggle('active', isActive);
p.hidden = !isActive; p.hidden = !isActive;
console.log('[TABS] Panel', p.id, 'active:', isActive); // noop
}); });
try { localStorage.setItem(TAB_STORAGE_KEY, tabName); } catch {} try { localStorage.setItem(TAB_STORAGE_KEY, tabName); } catch {}
} }
function initTabs() { function initTabs() {
console.log('[TABS] Initializing tabs...');
const links = document.querySelectorAll('.tab-link'); const links = document.querySelectorAll('.tab-link');
console.log('[TABS] Found tab links:', links.length);
// Direct listeners (for accessibility focus handling) // Direct listeners (for accessibility focus handling)
links.forEach((link, index) => { links.forEach((link, index) => {
console.log('[TABS] Setting up listener for tab', index, 'with data-tab:', link.dataset.tab);
link.addEventListener('click', (e) => { link.addEventListener('click', (e) => {
console.log('[TABS] Tab clicked:', link.dataset.tab);
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
const name = link.dataset.tab; const name = link.dataset.tab;
@@ -118,12 +112,9 @@ function initTabs() {
// Delegation as a fallback if elements are re-rendered // Delegation as a fallback if elements are re-rendered
const tabContainer = document.querySelector('.tabs'); const tabContainer = document.querySelector('.tabs');
if (tabContainer) { if (tabContainer) {
console.log('[TABS] Setting up delegation on tabs container');
tabContainer.addEventListener('click', (e) => { tabContainer.addEventListener('click', (e) => {
console.log('[TABS] Container click detected');
const btn = e.target && e.target.closest ? e.target.closest('.tab-link') : null; const btn = e.target && e.target.closest ? e.target.closest('.tab-link') : null;
if (!btn || !tabContainer.contains(btn)) return; if (!btn || !tabContainer.contains(btn)) return;
console.log('[TABS] Delegated click for tab:', btn.dataset.tab);
const name = btn.dataset.tab; const name = btn.dataset.tab;
if (!name) return; if (!name) return;
if (location.hash !== `#${name}`) { if (location.hash !== `#${name}`) {
@@ -139,13 +130,10 @@ function initTabs() {
try { initial = localStorage.getItem(TAB_STORAGE_KEY) || null; } catch {} try { initial = localStorage.getItem(TAB_STORAGE_KEY) || null; } catch {}
} }
if (!initial) initial = 'general'; if (!initial) initial = 'general';
console.log('[TABS] Initial tab:', initial);
activateTab(initial); activateTab(initial);
} }
// Initialize tabs after DOM is ready but before customization init uses the DOM // Initialize tabs after DOM is ready but before customization init uses the DOM
window.addEventListener('DOMContentLoaded', () => { window.addEventListener('DOMContentLoaded', () => {
console.log('[TABS] DOM loaded, initializing tabs...');
initTabs(); initTabs();
console.log('[TABS] Tabs initialized');
}); });
+22 -12
View File
@@ -28,7 +28,8 @@ html, body {
padding: 6px 10px; padding: 6px 10px;
background: #1e1e2e; background: #1e1e2e;
gap: 12px; gap: 12px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); /* flatter header to reduce paint cost */
box-shadow: none;
} }
.nav-left, .nav-left,
@@ -72,7 +73,7 @@ html, body {
padding: 6px 10px; padding: 6px 10px;
border-radius: 5px; border-radius: 5px;
cursor: pointer; cursor: pointer;
transition: background 0.2s; transition: background 120ms ease;
} }
#nav button:hover { #nav button:hover {
@@ -179,7 +180,8 @@ html, body {
margin: 0; margin: 0;
height: 28px; /* reduce overall tab height */ height: 28px; /* reduce overall tab height */
color: #ddd; color: #ddd;
background: linear-gradient(180deg, #24262b 0%, #1f2025 100%); /* use flat background to avoid gradient paints */
background: #24262b;
border: 1px solid #2b2d34; border: 1px solid #2b2d34;
border-bottom: none; /* let it visually merge with the strip line */ border-bottom: none; /* let it visually merge with the strip line */
border-radius: 6px 6px 0 0; /* slightly tighter radius */ border-radius: 6px 6px 0 0; /* slightly tighter radius */
@@ -189,17 +191,16 @@ html, body {
min-width: 120px; min-width: 120px;
flex: 0 1 180px; /* like Chrome: shrink when crowded */ flex: 0 1 180px; /* like Chrome: shrink when crowded */
overflow: hidden; overflow: hidden;
transition: background .15s ease, color .15s ease, box-shadow .15s ease; transition: background 120ms ease, color 120ms ease;
} }
.tab:hover { .tab:hover { background: #2a2c33; }
background: linear-gradient(180deg, #2a2c33 0%, #23252b 100%);
}
.tab.active { .tab.active {
color: #fff; color: #fff;
background: linear-gradient(180deg, #2f323a 0%, #2a2c33 100%); background: #2f323a;
box-shadow: 0 1px 0 rgba(255,255,255,0.04) inset, 0 0 0 1px #3a3d46 inset; /* remove expensive inner shadows */
box-shadow: none;
} }
.tab .tab-favicon { .tab .tab-favicon {
@@ -228,7 +229,7 @@ html, body {
background: transparent; background: transparent;
color: #b5b5b5; color: #b5b5b5;
opacity: 0; /* hidden by default */ opacity: 0; /* hidden by default */
transition: background .15s ease, color .15s ease, opacity .15s ease; transition: background 120ms ease, color 120ms ease, opacity 120ms ease;
} }
.tab:hover .tab-close, .tab:hover .tab-close,
@@ -249,7 +250,7 @@ html, body {
background: #23252b; background: #23252b;
color: #d5d5d5; color: #d5d5d5;
cursor: pointer; cursor: pointer;
transition: background .15s ease, color .15s ease, border-color .15s ease; transition: background 120ms ease, color 120ms ease, border-color 120ms ease;
} }
.new-tab-button:hover { background: #2b2d34; color: #fff; border-color: #3a3d46; } .new-tab-button:hover { background: #2b2d34; color: #fff; border-color: #3a3d46; }
.new-tab-button:active { background: #262830; } .new-tab-button:active { background: #262830; }
@@ -301,7 +302,7 @@ html, body {
color: white; color: white;
font-size: 12px; font-size: 12px;
cursor: pointer; cursor: pointer;
transition: background 0.2s; transition: background 120ms ease;
} }
#window-controls button:hover { #window-controls button:hover {
background: rgba(255,255,255,0.1); background: rgba(255,255,255,0.1);
@@ -324,3 +325,12 @@ html, body {
#tab-bar::-webkit-scrollbar-thumb:hover { #tab-bar::-webkit-scrollbar-thumb:hover {
background: #555; background: #555;
} }
/* Respect reduced motion preferences */
@media (prefers-reduced-motion: reduce) {
* {
animation: none !important;
transition: none !important;
scroll-behavior: auto !important;
}
}