From ac3924aaf7f555b6d21b578c02119c7fd05800ef Mon Sep 17 00:00:00 2001 From: alexsparkes Date: Wed, 29 Oct 2025 10:06:09 +0000 Subject: [PATCH] refactor: optimize background loading and rendering logic --- src/features/background/Background.jsx | 2 - .../background/api/backgroundLoader.js | 43 +++++++--- src/features/background/api/blobUrl.js | 14 +++- .../background/hooks/useBackgroundLoader.js | 25 +++--- .../background/hooks/useBackgroundRenderer.js | 81 ++++++++++++++----- src/features/background/scss/index.scss | 31 +++---- 6 files changed, 140 insertions(+), 56 deletions(-) diff --git a/src/features/background/Background.jsx b/src/features/background/Background.jsx index 8389d32c..058e07fa 100644 --- a/src/features/background/Background.jsx +++ b/src/features/background/Background.jsx @@ -1,5 +1,3 @@ -import { useEffect } from 'react'; - import BackgroundImage from './components/BackgroundImage'; import BackgroundVideo from './components/BackgroundVideo'; diff --git a/src/features/background/api/backgroundLoader.js b/src/features/background/api/backgroundLoader.js index dd5f3de1..b07bf49b 100644 --- a/src/features/background/api/backgroundLoader.js +++ b/src/features/background/api/backgroundLoader.js @@ -115,24 +115,49 @@ function getColourBackground() { } /** - * Gets API background with caching + * Gets API background with caching and prefetching */ async function getAPIBackground(isOffline) { if (isOffline) return getOfflineImage('api'); // Use cached next image if available - const cached = parseJSON('nextImage'); - const data = cached || (await fetchAPIImageData()); + const cachedQueue = parseJSON('imageQueue', []); + let data; + + if (cachedQueue.length > 0) { + data = cachedQueue.shift(); + localStorage.setItem('imageQueue', JSON.stringify(cachedQueue)); + } else { + data = await fetchAPIImageData(); + } if (!data) return getOfflineImage('api'); localStorage.setItem('currentBackground', JSON.stringify(data)); - localStorage.setItem('nextImage', null); - // Pre-fetch next image - fetchAPIImageData(data.photoInfo.pun).then((next) => { - if (next) localStorage.setItem('nextImage', JSON.stringify(next)); - }); + // Pre-fetch next 3 images in the background + const targetQueueSize = 3; + const currentQueueSize = cachedQueue.length; + + if (currentQueueSize < targetQueueSize) { + const excludedPuns = [ + data.photoInfo.pun, + ...cachedQueue.map((img) => img.photoInfo?.pun).filter(Boolean), + ]; + + // Prefetch remaining images asynchronously + Promise.all( + Array.from({ length: targetQueueSize - currentQueueSize }, (_, i) => + fetchAPIImageData(excludedPuns[i] || data.photoInfo.pun), + ), + ).then((newImages) => { + const validImages = newImages.filter(Boolean); + if (validImages.length > 0) { + const updatedQueue = [...cachedQueue, ...validImages]; + localStorage.setItem('imageQueue', JSON.stringify(updatedQueue)); + } + }); + } return data; } @@ -141,7 +166,7 @@ async function getAPIBackground(isOffline) { * Gets custom background */ function getCustomBackground(isOffline) { - let backgrounds = parseJSON('customBackground'); + const backgrounds = parseJSON('customBackground'); if (backgrounds.length === 0) return null; diff --git a/src/features/background/api/blobUrl.js b/src/features/background/api/blobUrl.js index c8ee5f73..713ca1b9 100644 --- a/src/features/background/api/blobUrl.js +++ b/src/features/background/api/blobUrl.js @@ -5,11 +5,19 @@ */ export async function createBlobUrl(url) { try { - const response = await fetch(url); + const response = await fetch(url, { + mode: 'cors', + credentials: 'omit', + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const blob = await response.blob(); return URL.createObjectURL(blob); - } catch (error) { - console.error('Failed to create blob URL:', error); + } catch { + // Silently fail - we'll use direct URL as fallback return null; } } diff --git a/src/features/background/hooks/useBackgroundLoader.js b/src/features/background/hooks/useBackgroundLoader.js index 223e0e1a..30cdf56c 100644 --- a/src/features/background/hooks/useBackgroundLoader.js +++ b/src/features/background/hooks/useBackgroundLoader.js @@ -13,14 +13,19 @@ export function useBackgroundLoader(updateBackground, resetBackground) { try { // Check for welcome tab first - const welcomeImage = localStorage.getItem('welcomeImage'); - if (localStorage.getItem('welcomeTab') && welcomeImage) { - updateBackground(JSON.parse(welcomeImage)); - return; + const welcomeTab = localStorage.getItem('welcomeTab'); + if (welcomeTab) { + const welcomeImage = localStorage.getItem('welcomeImage'); + if (welcomeImage) { + updateBackground(JSON.parse(welcomeImage)); + return; + } } const data = await getBackgroundData(); - if (data) updateBackground(data); + if (data) { + updateBackground(data); + } } catch (error) { console.error('Failed to load background:', error); } finally { @@ -36,12 +41,14 @@ export function useBackgroundLoader(updateBackground, resetBackground) { // Initial load - only run once on mount useEffect(() => { const changeMode = localStorage.getItem('backgroundchange'); - if (changeMode === 'refresh' || !changeMode) { + const hasStartTime = localStorage.getItem('backgroundStartTime'); + + if (!hasStartTime || changeMode === 'refresh') { localStorage.setItem('backgroundStartTime', Date.now()); - loadBackground(); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + + loadBackground(); + }, [loadBackground]); return { loadBackground, refreshBackground }; } diff --git a/src/features/background/hooks/useBackgroundRenderer.js b/src/features/background/hooks/useBackgroundRenderer.js index eb0e6049..93f05602 100644 --- a/src/features/background/hooks/useBackgroundRenderer.js +++ b/src/features/background/hooks/useBackgroundRenderer.js @@ -15,12 +15,6 @@ export function useBackgroundRenderer(backgroundData) { const element = document.getElementById('backgroundImage'); if (!element) return; - // Cleanup previous blob - if (blobRef.current) { - URL.revokeObjectURL(blobRef.current); - blobRef.current = null; - } - const applyBackground = async () => { if (backgroundData.url) { const hasTransition = localStorage.getItem('bgtransition') !== 'false'; @@ -31,26 +25,77 @@ export function useBackgroundRenderer(backgroundData) { return; } - element.style.background = null; + // Create or get overlay element for smooth transitions + let overlay = document.getElementById('backgroundOverlay'); + if (!overlay) { + overlay = document.createElement('div'); + overlay.id = 'backgroundOverlay'; + // Insert right after the background element + element.parentNode.insertBefore(overlay, element.nextSibling); + } - // Apply blur hash placeholder - if (backgroundData.photoInfo?.blur_hash && backgroundData.photoInfo?.colour) { + // Set background color + if (backgroundData.photoInfo?.colour) { element.style.backgroundColor = backgroundData.photoInfo.colour; - element.classList.add('fade-in'); - - const blurHash = generateBlurHashDataUrl(backgroundData.photoInfo.blur_hash); - if (blurHash) element.style.backgroundImage = `url(${blurHash})`; } - // Load full image + // Generate and show blur hash immediately on main element + if (backgroundData.photoInfo?.blur_hash) { + const blurHashUrl = generateBlurHashDataUrl(backgroundData.photoInfo.blur_hash, 64, 64); + if (blurHashUrl) { + element.style.backgroundImage = `url(${blurHashUrl})`; + } + } + + // Load the full image const blobUrl = await createBlobUrl(backgroundData.url); + const finalUrl = blobUrl || backgroundData.url; + if (blobUrl) { + if (blobRef.current) { + URL.revokeObjectURL(blobRef.current); + } blobRef.current = blobUrl; - element.classList.add('backgroundTransform'); - element.style.backgroundImage = `url(${blobUrl})`; - } else { - element.style.backgroundImage = `url(${backgroundData.url})`; } + + // Preload the image + const img = new Image(); + img.src = finalUrl; + + await new Promise((resolve) => { + img.onload = resolve; + img.onerror = resolve; + }); + + // CRITICAL: Reset overlay to invisible FIRST + overlay.style.transition = 'none'; + overlay.style.opacity = '0'; + overlay.style.backgroundImage = ''; + + // Force a reflow to ensure opacity is actually 0 + void overlay.offsetHeight; + + // Now set the new image + overlay.style.backgroundImage = `url(${finalUrl})`; + + // Force another reflow + void overlay.offsetHeight; + + // Re-enable transition + overlay.style.transition = 'opacity 1.2s ease-in-out'; + + // Force another reflow before changing opacity + void overlay.offsetHeight; + + // Now fade it in + overlay.style.opacity = '1'; + + // After fade completes, swap to main element and reset overlay + setTimeout(() => { + element.style.backgroundImage = `url(${finalUrl})`; + overlay.style.opacity = '0'; + overlay.style.backgroundImage = ''; + }, 1300); } else if (backgroundData.style) { element.setAttribute('style', backgroundData.style); } diff --git a/src/features/background/scss/index.scss b/src/features/background/scss/index.scss index 96f57967..2dea870e 100644 --- a/src/features/background/scss/index.scss +++ b/src/features/background/scss/index.scss @@ -8,22 +8,23 @@ background-position: center !important; background-attachment: fixed !important; zoom: 100% !important; + position: relative; + z-index: -2; } -.backgroundTransform { - @include animation('fadein2 2s'); - - transition: background 2s; -} - -@include keyframes(fadein2) { - from { - opacity: 0; - } - - to { - opacity: 1; - } +#backgroundOverlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100vh; + background-size: cover !important; + background-repeat: no-repeat !important; + background-position: center !important; + background-attachment: fixed !important; + pointer-events: none; + z-index: -1; + transition: opacity 1.2s ease-in-out; } .backgroundPreload { @@ -39,7 +40,7 @@ } .fade-in { - @include animation('fadein 1s'); + @include animation('fadein 0.8s ease-in-out'); } @include keyframes(fadein) {