Merge pull request #783 from nick2000713/perf/webui-navigation-scroll
perf(webui): faster navigation, smoother scroll, fix spurious settings save on load
This commit is contained in:
commit
4c65f403e1
9 changed files with 219 additions and 56 deletions
|
|
@ -382,7 +382,7 @@ function initializeWebSocket() {
|
|||
}
|
||||
|
||||
socket = io({
|
||||
transports: ['polling', 'websocket'],
|
||||
transports: ['websocket', 'polling'],
|
||||
reconnection: true,
|
||||
reconnectionAttempts: Infinity,
|
||||
reconnectionDelay: 1000,
|
||||
|
|
|
|||
|
|
@ -5802,12 +5802,12 @@ function _gsUpdateVisibility() {
|
|||
const bar = document.getElementById('gsearch-bar');
|
||||
const aura = document.getElementById('gsearch-aura');
|
||||
if (!bar) return;
|
||||
// Hide on the Search page where the unified search already exists. Accept the
|
||||
// legacy 'downloads' id for callers that predate the page rename.
|
||||
const onSearchPage = typeof currentPage !== 'undefined' && (currentPage === 'search' || currentPage === 'downloads');
|
||||
bar.style.display = onSearchPage ? 'none' : '';
|
||||
if (aura) aura.classList.toggle('hidden', onSearchPage);
|
||||
if (onSearchPage && _gsState.active) _gsDeactivate();
|
||||
// Hide on pages where global search doesn't belong.
|
||||
const _gsHidePages = new Set(['search', 'downloads', 'settings', 'help', 'issues', 'import']);
|
||||
const onHidePage = typeof currentPage !== 'undefined' && _gsHidePages.has(currentPage);
|
||||
bar.style.display = onHidePage ? 'none' : '';
|
||||
if (aura) aura.classList.toggle('hidden', onHidePage);
|
||||
if (onHidePage && _gsState.active) _gsDeactivate();
|
||||
}
|
||||
|
||||
function _gsDeactivate() {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,18 @@
|
|||
// INITIALIZATION
|
||||
// ===============================
|
||||
let navigationEpoch = 0;
|
||||
let _optimisticNavPageId = null;
|
||||
|
||||
// Schedule heavy per-page init during browser idle time so navigation paints and
|
||||
// the page becomes scrollable first. timeout caps the delay so content still loads
|
||||
// promptly. Falls back to a macrotask in browsers without requestIdleCallback.
|
||||
function _scheduleHeavyInit(fn) {
|
||||
if (typeof requestIdleCallback === 'function') {
|
||||
requestIdleCallback(fn, { timeout: 200 });
|
||||
} else {
|
||||
setTimeout(fn, 0);
|
||||
}
|
||||
}
|
||||
|
||||
function notifyPageWillChange(nextPageId) {
|
||||
const fromPageId = typeof currentPage === 'string' ? currentPage : null;
|
||||
|
|
@ -175,13 +187,35 @@ function applyReduceEffects(enabled) {
|
|||
} else {
|
||||
document.body.classList.remove('reduce-effects');
|
||||
}
|
||||
window._reduceEffectsActive = enabled;
|
||||
localStorage.setItem('soulsync-reduce-effects', enabled ? '1' : '0');
|
||||
|
||||
// Reduce Visual Effects is a full performance switch: also halt the canvas
|
||||
// animation loops (particles + worker orbs), not just CSS effects.
|
||||
const pcanvas = document.getElementById('page-particles-canvas');
|
||||
if (enabled) {
|
||||
if (window.pageParticles) window.pageParticles.stop();
|
||||
if (pcanvas) pcanvas.style.display = 'none';
|
||||
if (window.workerOrbs) window.workerOrbs.setPage('_disabled');
|
||||
} else {
|
||||
// Restore only what the user's own toggles still allow.
|
||||
const activePage = document.querySelector('.page.active');
|
||||
const activeId = activePage ? activePage.id.replace('-page', '') : null;
|
||||
if (window._particlesEnabled !== false) {
|
||||
if (pcanvas) pcanvas.style.display = '';
|
||||
if (window.pageParticles && activeId) window.pageParticles.setPage(activeId);
|
||||
}
|
||||
if (window._workerOrbsEnabled !== false && window.workerOrbs && activeId) {
|
||||
window.workerOrbs.setPage(activeId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Bootstrap accent and reduce-effects from localStorage instantly (prevents flash)
|
||||
(function () {
|
||||
if (localStorage.getItem('soulsync-reduce-effects') === '1') {
|
||||
document.body.classList.add('reduce-effects');
|
||||
window._reduceEffectsActive = true;
|
||||
}
|
||||
const saved = localStorage.getItem('soulsync-accent');
|
||||
if (saved) applyAccentColor(saved);
|
||||
|
|
@ -2314,6 +2348,15 @@ function navigateToPage(pageId, options = {}) {
|
|||
if (route?.kind === 'react') {
|
||||
showReactHost(pageId);
|
||||
setActivePageChrome(pageId);
|
||||
} else if (route?.kind === 'legacy' && pageId !== 'artist-detail') {
|
||||
// Show legacy page immediately — don't wait for TanStack Router's async cycle
|
||||
showLegacyPage(pageId);
|
||||
setActivePageChrome(pageId);
|
||||
_optimisticNavPageId = pageId;
|
||||
// Defer data loading until the browser is idle, so the page paints AND
|
||||
// becomes scrollable before heavy sync init (settings form wiring, etc.)
|
||||
// runs. Falls back to a macrotask where requestIdleCallback is missing.
|
||||
_scheduleHeavyInit(() => loadPageData(pageId));
|
||||
}
|
||||
return router.navigateToPage(pageId, {
|
||||
replace: options.replace === true,
|
||||
|
|
@ -2413,12 +2456,20 @@ async function loadPageData(pageId) {
|
|||
initExplorer();
|
||||
break;
|
||||
case 'settings':
|
||||
initializeSettings();
|
||||
switchSettingsTab('connections');
|
||||
await loadSettingsData();
|
||||
await loadQualityProfile();
|
||||
loadApiKeys();
|
||||
loadBlacklistCount();
|
||||
// Suppress auto-save while the form is being populated, so opening
|
||||
// Settings no longer fires a spurious full save (4 POSTs + backend
|
||||
// service re-init) on every visit.
|
||||
window._suppressSettingsAutoSave = true;
|
||||
try {
|
||||
initializeSettings();
|
||||
switchSettingsTab('connections');
|
||||
await loadSettingsData();
|
||||
await loadQualityProfile();
|
||||
loadApiKeys();
|
||||
loadBlacklistCount();
|
||||
} finally {
|
||||
window._suppressSettingsAutoSave = false;
|
||||
}
|
||||
break;
|
||||
case 'hydrabase':
|
||||
// Check connection status and pre-fill saved credentials
|
||||
|
|
|
|||
|
|
@ -69,13 +69,32 @@
|
|||
return result;
|
||||
}
|
||||
|
||||
// Cache viewport dims so initLayer/setPreset (runs on every page nav) doesn't
|
||||
// force a synchronous layout flush by reading clientWidth/clientHeight each time.
|
||||
let _vpW = 0, _vpH = 0;
|
||||
function measureViewport() {
|
||||
_vpW = canvas.clientWidth;
|
||||
_vpH = canvas.clientHeight;
|
||||
}
|
||||
function resize() {
|
||||
const dpr = 1; // keep 1:1 for performance
|
||||
canvas.width = canvas.clientWidth * dpr;
|
||||
canvas.height = canvas.clientHeight * dpr;
|
||||
if (_vpW === 0) measureViewport(); // first run only
|
||||
if (canvas.width !== _vpW || canvas.height !== _vpH) {
|
||||
canvas.width = _vpW;
|
||||
canvas.height = _vpH;
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('resize', resize);
|
||||
let _resizeRaf = 0;
|
||||
window.addEventListener('resize', () => {
|
||||
// Debounce to a frame; re-measure (reads layout) then apply.
|
||||
if (_resizeRaf) return;
|
||||
_resizeRaf = requestAnimationFrame(() => {
|
||||
_resizeRaf = 0;
|
||||
measureViewport();
|
||||
resize();
|
||||
});
|
||||
});
|
||||
measureViewport();
|
||||
resize();
|
||||
|
||||
// ── Preset Definitions ──
|
||||
|
|
@ -2061,9 +2080,31 @@
|
|||
return { preset, particles: parts, extras: layerExtras, name: presetName };
|
||||
}
|
||||
|
||||
function loop() {
|
||||
let _lastFrameTime = 0;
|
||||
const _targetFps = 30;
|
||||
const _frameInterval = 1000 / _targetFps;
|
||||
|
||||
// Pause particle drawing during active scroll (timestamp threshold, ms).
|
||||
let _scrollPauseUntil = 0;
|
||||
(function attachScrollPause() {
|
||||
const scroller = document.querySelector('.main-content') || window;
|
||||
scroller.addEventListener('scroll', () => {
|
||||
// requestAnimationFrame timestamps share the performance.now() clock.
|
||||
_scrollPauseUntil = performance.now() + 180;
|
||||
}, { passive: true });
|
||||
})();
|
||||
|
||||
function loop(timestamp) {
|
||||
animFrame = requestAnimationFrame(loop);
|
||||
|
||||
// Give the scroll its full frame budget: while the user is actively
|
||||
// scrolling, skip the full-viewport canvas redraw (particles freeze on
|
||||
// their last frame, then resume on scroll-idle). Eliminates scroll jank.
|
||||
if (timestamp < _scrollPauseUntil) return;
|
||||
|
||||
if (timestamp - _lastFrameTime < _frameInterval) return;
|
||||
_lastFrameTime = timestamp - ((timestamp - _lastFrameTime) % _frameInterval);
|
||||
|
||||
const w = canvas.width, h = canvas.height;
|
||||
if (w === 0 || h === 0) { resize(); return; }
|
||||
|
||||
|
|
@ -2286,6 +2327,8 @@
|
|||
// Listen for page changes from script.js
|
||||
window.pageParticles = {
|
||||
setPage(pageId) {
|
||||
// Reduce Visual Effects performance mode halts the loop entirely.
|
||||
if (window._reduceEffectsActive) { stop(); return; }
|
||||
const presetName = PAGE_PRESETS[pageId] || 'none';
|
||||
setPreset(presetName);
|
||||
},
|
||||
|
|
@ -2294,7 +2337,7 @@
|
|||
|
||||
// Auto-start for initial page (respect particles toggle)
|
||||
requestAnimationFrame(() => {
|
||||
if (window._particlesEnabled === false) return;
|
||||
if (window._particlesEnabled === false || window._reduceEffectsActive) return;
|
||||
const activePage = document.querySelector('.page.active');
|
||||
if (activePage) {
|
||||
const pageId = activePage.id.replace('-page', '');
|
||||
|
|
|
|||
|
|
@ -41,6 +41,10 @@ async function copyAddress(address, cryptoName) {
|
|||
let settingsAutoSaveTimer = null;
|
||||
|
||||
function debouncedAutoSaveSettings() {
|
||||
// Ignore changes made while the page is programmatically populating its
|
||||
// fields on load — those aren't user edits and must not trigger a full
|
||||
// save (which re-initializes every backend service client).
|
||||
if (window._suppressSettingsAutoSave) return;
|
||||
if (settingsAutoSaveTimer) clearTimeout(settingsAutoSaveTimer);
|
||||
settingsAutoSaveTimer = setTimeout(() => saveSettings(true), 2000);
|
||||
}
|
||||
|
|
@ -138,9 +142,14 @@ function handleMetadataSourceChange(event) {
|
|||
sanitizeMetadataSourceSelection({ quiet: false });
|
||||
}
|
||||
|
||||
let _settingsInitialized = false;
|
||||
function initializeSettings() {
|
||||
// This function is called when the settings page is loaded.
|
||||
// It attaches event listeners to all interactive elements on the page.
|
||||
// Listeners are stable for the page lifetime, so wiring them once avoids
|
||||
// re-scanning the ~960-node settings subtree on every revisit (scroll jank).
|
||||
if (_settingsInitialized) return;
|
||||
_settingsInitialized = true;
|
||||
|
||||
// Accent color listeners (live preview + custom picker toggle)
|
||||
initAccentColorListeners();
|
||||
|
|
|
|||
|
|
@ -52,8 +52,11 @@ function setActivePageChrome(pageId) {
|
|||
downloadSidebar.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
if (window.pageParticles && window._particlesEnabled !== false) window.pageParticles.setPage(pageId);
|
||||
if (window.workerOrbs) window.workerOrbs.setPage(pageId);
|
||||
// Defer to next frame so the page switch paints before particle/orb reinitialization
|
||||
requestAnimationFrame(() => {
|
||||
if (window.pageParticles && window._particlesEnabled !== false) window.pageParticles.setPage(pageId);
|
||||
if (window.workerOrbs) window.workerOrbs.setPage(pageId);
|
||||
});
|
||||
}
|
||||
|
||||
function showReactHost(pageId) {
|
||||
|
|
@ -83,6 +86,14 @@ function activateLegacyPath(pathname) {
|
|||
return;
|
||||
}
|
||||
|
||||
// If the page was already shown optimistically (forward nav), skip re-activation
|
||||
// to avoid a duplicate data load. Back-button nav always has _optimisticNavPageId = null.
|
||||
if (typeof _optimisticNavPageId !== 'undefined' && _optimisticNavPageId === targetPage) {
|
||||
_optimisticNavPageId = null;
|
||||
return;
|
||||
}
|
||||
_optimisticNavPageId = null;
|
||||
|
||||
notifyPageWillChange(targetPage);
|
||||
activatePage(targetPage, { forceReload: true });
|
||||
}
|
||||
|
|
@ -241,4 +252,15 @@ function _isModifiedLinkClick(event) {
|
|||
|
||||
window.addEventListener('popstate', syncActivePageFromLocation);
|
||||
document.addEventListener('click', _handleShellLinkClick, true);
|
||||
|
||||
// Fire nav on pointerdown (fires on press, 100-200ms before click) for instant sidebar response.
|
||||
// navigateToPage's early-return guard (pageId === currentPage) prevents double-navigation on click.
|
||||
document.addEventListener('pointerdown', (event) => {
|
||||
if (event.button !== 0 || _isModifiedLinkClick(event)) return;
|
||||
const btn = event.target?.closest?.('.nav-button[data-page]');
|
||||
if (!btn) return;
|
||||
const pageId = btn.getAttribute('data-page');
|
||||
if (pageId) void navigateToPage(pageId);
|
||||
}, true);
|
||||
|
||||
window.dispatchEvent(new CustomEvent(SHELL_BRIDGE_READY_EVENT));
|
||||
|
|
|
|||
|
|
@ -133,6 +133,13 @@ body {
|
|||
z-index: 0;
|
||||
}
|
||||
|
||||
/* In performance mode the aura loses its blur + animation, leaving two hard
|
||||
static circles — hide them entirely instead. */
|
||||
body.reduce-effects .sidebar::before,
|
||||
body.reduce-effects .sidebar::after {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@keyframes sidebar-orb-1 {
|
||||
0%, 100% { transform: translate(0, 0) scale(1); opacity: 0.5; }
|
||||
25% { transform: translate(30px, 80px) scale(1.2); opacity: 1; }
|
||||
|
|
@ -388,7 +395,7 @@ body {
|
|||
gap: 14px;
|
||||
text-decoration: none;
|
||||
padding: 0 16px;
|
||||
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition: background 0.1s ease, border-color 0.1s ease, transform 0.1s ease, box-shadow 0.1s ease, color 0.1s ease;
|
||||
font-family: 'SF Pro Text', -apple-system, sans-serif;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
|
|
@ -408,14 +415,14 @@ body {
|
|||
border-radius: 0 3px 3px 0;
|
||||
background: linear-gradient(180deg, rgb(var(--accent-light-rgb)), rgb(var(--accent-rgb)));
|
||||
box-shadow: 0 0 8px rgba(var(--accent-rgb), 0.6);
|
||||
transition: height 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition: height 0.12s ease;
|
||||
}
|
||||
|
||||
.nav-button.active::before {
|
||||
height: 60%;
|
||||
}
|
||||
|
||||
.nav-button:hover {
|
||||
body:not(.reduce-effects) .nav-button:hover {
|
||||
background: linear-gradient(135deg,
|
||||
rgba(255, 255, 255, 0.06) 0%,
|
||||
rgba(255, 255, 255, 0.03) 100%);
|
||||
|
|
@ -426,20 +433,32 @@ body {
|
|||
inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.nav-button:active:not(.active) {
|
||||
background: linear-gradient(135deg,
|
||||
rgba(var(--accent-rgb), 0.14) 0%,
|
||||
rgba(var(--accent-rgb), 0.10) 50%,
|
||||
rgba(var(--accent-rgb), 0.06) 100%);
|
||||
border: 1px solid rgba(var(--accent-rgb), 0.2);
|
||||
box-shadow:
|
||||
0 4px 16px rgba(var(--accent-rgb), 0.2),
|
||||
0 2px 8px rgba(0, 0, 0, 0.2),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.nav-button.active {
|
||||
background: linear-gradient(135deg,
|
||||
rgba(var(--accent-rgb), 0.14) 0%,
|
||||
rgba(var(--accent-rgb), 0.10) 50%,
|
||||
rgba(var(--accent-rgb), 0.06) 100%);
|
||||
border: 1px solid rgba(var(--accent-rgb), 0.2);
|
||||
transform: translateX(6px);
|
||||
box-shadow:
|
||||
0 4px 16px rgba(var(--accent-rgb), 0.2),
|
||||
0 2px 8px rgba(0, 0, 0, 0.2),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.nav-button.active:hover {
|
||||
body:not(.reduce-effects) .nav-button.active:hover {
|
||||
background: linear-gradient(135deg,
|
||||
rgba(var(--accent-rgb), 0.20) 0%,
|
||||
rgba(var(--accent-rgb), 0.16) 50%,
|
||||
|
|
@ -466,7 +485,7 @@ body {
|
|||
rgba(255, 255, 255, 0.03) 100%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition: background 0.1s ease, border-color 0.1s ease, color 0.1s ease, box-shadow 0.1s ease;
|
||||
box-shadow:
|
||||
0 2px 4px rgba(0, 0, 0, 0.15),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
||||
|
|
@ -476,7 +495,7 @@ body {
|
|||
width: 22px;
|
||||
height: 22px;
|
||||
pointer-events: none;
|
||||
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition: color 0.1s ease, opacity 0.1s ease;
|
||||
color: rgba(var(--accent-rgb), 0.7);
|
||||
}
|
||||
|
||||
|
|
@ -4925,6 +4944,8 @@ body.helper-mode-active #dashboard-activity-feed:hover {
|
|||
border: 1px solid rgba(70, 70, 76, 0.5);
|
||||
margin-bottom: 8px;
|
||||
padding: 16px;
|
||||
content-visibility: auto;
|
||||
contain-intrinsic-size: auto 88px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
|
@ -20446,6 +20467,8 @@ body.helper-mode-active #dashboard-activity-feed:hover {
|
|||
/* Wishlist Delete Buttons */
|
||||
.wishlist-track-item {
|
||||
position: relative;
|
||||
content-visibility: auto;
|
||||
contain-intrinsic-size: auto 64px;
|
||||
}
|
||||
|
||||
.wishlist-delete-btn {
|
||||
|
|
@ -26482,6 +26505,8 @@ body.helper-mode-active #dashboard-activity-feed:hover {
|
|||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
content-visibility: auto;
|
||||
contain-intrinsic-size: auto 240px;
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
border-color 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
|
@ -33658,6 +33683,10 @@ div.artist-hero-badge {
|
|||
.discover-section {
|
||||
margin-bottom: 50px;
|
||||
padding: 0 20px;
|
||||
/* Skip layout/paint of off-screen shelves so entering Discover and scrolling
|
||||
stays cheap. 'auto' lets the browser remember real size after first render. */
|
||||
content-visibility: auto;
|
||||
contain-intrinsic-size: auto 340px;
|
||||
}
|
||||
|
||||
.discover-section-header {
|
||||
|
|
@ -58725,17 +58754,23 @@ tr:hover .enhanced-track-actions-group { opacity: 1; }
|
|||
}
|
||||
.blacklist-entry-remove:hover { background: rgba(239, 83, 80, 0.12); color: #ef5350; }
|
||||
|
||||
/* ── Reduce Visual Effects ── Disables GPU-heavy properties globally */
|
||||
/* ── Reduce Visual Effects ── Full performance mode: disables GPU-heavy
|
||||
properties and halts decorative animation globally. */
|
||||
body.reduce-effects *,
|
||||
body.reduce-effects *::before,
|
||||
body.reduce-effects *::after {
|
||||
animation-duration: 0s !important;
|
||||
animation-delay: 0s !important;
|
||||
animation: none !important;
|
||||
transition-duration: 0s !important;
|
||||
transition-delay: 0s !important;
|
||||
backdrop-filter: none !important;
|
||||
-webkit-backdrop-filter: none !important;
|
||||
box-shadow: none !important;
|
||||
filter: none !important;
|
||||
}
|
||||
|
||||
/* Kill the background particle canvas entirely in performance mode */
|
||||
body.reduce-effects #page-particles-canvas {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
|
|
|
|||
|
|
@ -7495,35 +7495,27 @@ async function initializeToolsPage() {
|
|||
}
|
||||
|
||||
async function loadDashboardData() {
|
||||
// Initial load of wishlist count
|
||||
await updateWishlistCount();
|
||||
|
||||
// Start periodic refresh of wishlist count (every 10 seconds)
|
||||
// Start periodic refreshers up front (independent of the initial loads).
|
||||
stopWishlistCountPolling(); // Ensure no duplicates
|
||||
wishlistCountInterval = setInterval(updateWishlistCount, 10000);
|
||||
|
||||
// Initial load of service status, system statistics, and library status
|
||||
await fetchAndUpdateServiceStatus();
|
||||
await fetchAndUpdateSystemStats();
|
||||
await fetchAndUpdateDbStats();
|
||||
|
||||
// Service status is already polled globally (line 311)
|
||||
// System stats polling kept here (dashboard-specific)
|
||||
setInterval(fetchAndUpdateSystemStats, 10000);
|
||||
|
||||
// Initial load of activity feed
|
||||
await fetchAndUpdateActivityFeed();
|
||||
|
||||
// Start periodic refresh of activity feed (every 2 seconds for responsiveness)
|
||||
setInterval(fetchAndUpdateActivityFeed, 2000);
|
||||
|
||||
// Start periodic toast checking (every 3 seconds)
|
||||
setInterval(fetchAndUpdateSystemStats, 10000); // dashboard-specific (service status polled globally)
|
||||
setInterval(fetchAndUpdateActivityFeed, 2000); // responsive activity feed
|
||||
setInterval(checkForActivityToasts, 3000);
|
||||
|
||||
// Check for any active download processes that need rehydration
|
||||
await checkForActiveProcesses();
|
||||
// Fire all independent initial loads in parallel instead of sequentially.
|
||||
// Sequential awaits meant 6 back-to-back round-trips, each triggering its own
|
||||
// reflow — the layout kept shifting for ~1-2s, which made the page feel
|
||||
// unscrollable. Concurrent loads collapse that into a single settle.
|
||||
await Promise.all([
|
||||
updateWishlistCount(),
|
||||
fetchAndUpdateServiceStatus(),
|
||||
fetchAndUpdateSystemStats(),
|
||||
fetchAndUpdateDbStats(),
|
||||
fetchAndUpdateActivityFeed(),
|
||||
checkForActiveProcesses(),
|
||||
]);
|
||||
|
||||
// Populate the Active Downloads dashboard section with any existing downloads
|
||||
// Render existing downloads once active processes are known.
|
||||
updateDashboardDownloads();
|
||||
|
||||
// Automatic wishlist processing now runs server-side
|
||||
|
|
|
|||
|
|
@ -439,6 +439,14 @@
|
|||
|
||||
let frameCount = 0;
|
||||
|
||||
let _scrollPauseUntil = 0;
|
||||
(function attachScrollPause() {
|
||||
const scroller = document.querySelector('.main-content') || window;
|
||||
scroller.addEventListener('scroll', () => {
|
||||
_scrollPauseUntil = performance.now() + 180;
|
||||
}, { passive: true });
|
||||
})();
|
||||
|
||||
function startLoop() {
|
||||
if (animFrame) return;
|
||||
tick();
|
||||
|
|
@ -455,6 +463,9 @@
|
|||
animFrame = requestAnimationFrame(tick);
|
||||
if (!canvas || !ctx) return;
|
||||
|
||||
// Yield the frame to active scrolling (orbs freeze, resume on idle).
|
||||
if (performance.now() < _scrollPauseUntil) return;
|
||||
|
||||
frameCount++;
|
||||
const time = frameCount / 60;
|
||||
const w = canvas.width;
|
||||
|
|
@ -840,7 +851,7 @@
|
|||
// ── Page awareness ──
|
||||
|
||||
function isEnabled() {
|
||||
return window._workerOrbsEnabled !== false;
|
||||
return window._workerOrbsEnabled !== false && !window._reduceEffectsActive;
|
||||
}
|
||||
|
||||
// ── Real telemetry → pulses ──
|
||||
|
|
|
|||
Loading…
Reference in a new issue