perf : deferred slide rendering + sequential loading by proximity. related to #55
All checks were successful
Deploy / Deploy to Production (push) Successful in 5m26s
All checks were successful
Deploy / Deploy to Production (push) Successful in 5m26s
Only render the active slide initially. After its critical media (videos) fires canplaythrough, progressively render remaining slides by distance. JSON loading is now sequential by proximity instead of all-parallel. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f3ce36b99c
commit
947275544d
3 changed files with 114 additions and 5 deletions
|
|
@ -53,6 +53,7 @@
|
|||
let isReady = $state(false)
|
||||
let isResizing = $state(false)
|
||||
let resizeTimer = null
|
||||
let mediaCheckCleanup = null
|
||||
|
||||
// Active la transition seulement après le premier paint à la bonne position.
|
||||
// Double rAF : le premier laisse passer un paint avec le bon translateX,
|
||||
|
|
@ -63,6 +64,58 @@
|
|||
}
|
||||
})
|
||||
|
||||
// Détecte quand les médias critiques de la slide active sont prêts,
|
||||
// puis déclenche le rendu progressif des autres slides.
|
||||
$effect(() => {
|
||||
const idx = slides.activeIndex
|
||||
const all = slides.all
|
||||
|
||||
// Cleanup du check précédent
|
||||
if (mediaCheckCleanup) { mediaCheckCleanup(); mediaCheckCleanup = null }
|
||||
|
||||
if (all.length === 0 || !all[idx]?.loaded) return
|
||||
|
||||
// Attendre un tick pour que le composant soit monté dans le DOM
|
||||
const rafId = requestAnimationFrame(() => {
|
||||
const section = document.querySelector(`[data-slide="${all[idx].id}"]`)
|
||||
if (!section) { slides.setActiveMediaReady(true); return }
|
||||
|
||||
const videos = section.querySelectorAll('video')
|
||||
if (videos.length === 0) { slides.setActiveMediaReady(true); return }
|
||||
|
||||
let settled = false
|
||||
const settle = () => {
|
||||
if (settled) return
|
||||
settled = true
|
||||
slides.setActiveMediaReady(true)
|
||||
}
|
||||
|
||||
// Fallback timeout
|
||||
const timeout = setTimeout(settle, 5000)
|
||||
|
||||
// Attendre canplaythrough sur toutes les vidéos visibles
|
||||
let remaining = 0
|
||||
const onReady = () => { if (--remaining <= 0) settle() }
|
||||
|
||||
for (const v of videos) {
|
||||
// Ignorer les vidéos cachées (display: none)
|
||||
if (v.offsetParent === null && getComputedStyle(v).display === 'none') continue
|
||||
if (v.readyState >= 4) continue
|
||||
remaining++
|
||||
v.addEventListener('canplaythrough', onReady, { once: true })
|
||||
}
|
||||
|
||||
if (remaining === 0) settle()
|
||||
|
||||
mediaCheckCleanup = () => {
|
||||
clearTimeout(timeout)
|
||||
for (const v of videos) v.removeEventListener('canplaythrough', onReady)
|
||||
}
|
||||
})
|
||||
|
||||
mediaCheckCleanup = () => cancelAnimationFrame(rafId)
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
const handleResize = () => {
|
||||
isResizing = true
|
||||
|
|
@ -161,7 +214,7 @@
|
|||
>
|
||||
{#each slides.all as slide, i}
|
||||
<section class="slide" class:active={i === slides.activeIndex} data-slide={slide.id} inert={i !== slides.activeIndex}>
|
||||
{#if slide.loaded}
|
||||
{#if slide.loaded && slides.isRenderable(i)}
|
||||
<svelte:component
|
||||
this={templates[slide.template] ?? Default}
|
||||
data={slide.data}
|
||||
|
|
|
|||
|
|
@ -76,10 +76,16 @@ async function loadSlide(path) {
|
|||
}
|
||||
}
|
||||
|
||||
function loadAllSlidesInBackground(exceptPath) {
|
||||
slides.all
|
||||
.filter((s) => s.path !== exceptPath)
|
||||
.forEach((s) => loadSlide(s.path));
|
||||
async function loadAllSlidesInBackground(exceptPath) {
|
||||
const activeIdx = slides.getIndexByPath(exceptPath)
|
||||
const remaining = slides.all
|
||||
.map((s, i) => ({ path: s.path, distance: Math.abs(i - activeIdx) }))
|
||||
.filter(s => s.path !== exceptPath)
|
||||
.sort((a, b) => a.distance - b.distance)
|
||||
|
||||
for (const { path } of remaining) {
|
||||
await loadSlide(path)
|
||||
}
|
||||
}
|
||||
|
||||
export function slideTo(path, { skipHistory = false } = {}) {
|
||||
|
|
|
|||
|
|
@ -3,20 +3,64 @@ let activeIndex = $state(0)
|
|||
let pendingPath = $state(null)
|
||||
let standaloneData = $state(null)
|
||||
|
||||
// --- Deferred rendering ---
|
||||
let renderableSet = $state(new Set())
|
||||
let activeMediaReady = $state(false)
|
||||
let progressiveTimers = []
|
||||
|
||||
function getIndexByPath(path) {
|
||||
return slidesData.findIndex(s => s.path === path)
|
||||
}
|
||||
|
||||
function renderProgressively() {
|
||||
progressiveTimers.forEach(clearTimeout)
|
||||
progressiveTimers = []
|
||||
|
||||
const total = slidesData.length
|
||||
if (total === 0) return
|
||||
|
||||
// Build list sorted by distance from active, excluding already renderable
|
||||
const toRender = []
|
||||
for (let dist = 1; dist < total; dist++) {
|
||||
for (const i of [activeIndex - dist, activeIndex + dist]) {
|
||||
if (i >= 0 && i < total && !renderableSet.has(i)) {
|
||||
toRender.push(i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toRender.forEach((idx, order) => {
|
||||
const timer = setTimeout(() => {
|
||||
renderableSet = new Set([...renderableSet, idx])
|
||||
}, (order + 1) * 200)
|
||||
progressiveTimers.push(timer)
|
||||
})
|
||||
}
|
||||
|
||||
export const slides = {
|
||||
get all() { return slidesData },
|
||||
get activeIndex() { return activeIndex },
|
||||
get active() { return slidesData[activeIndex] ?? null },
|
||||
get pendingPath() { return pendingPath },
|
||||
get standalone() { return standaloneData },
|
||||
get activeMediaReady() { return activeMediaReady },
|
||||
|
||||
setStandalone(data) { standaloneData = data },
|
||||
clearStandalone() { standaloneData = null },
|
||||
|
||||
isRenderable(i) { return renderableSet.has(i) },
|
||||
|
||||
setActiveMediaReady(ready) {
|
||||
activeMediaReady = ready
|
||||
if (ready) renderProgressively()
|
||||
},
|
||||
|
||||
markRenderable(i) {
|
||||
if (!renderableSet.has(i)) {
|
||||
renderableSet = new Set([...renderableSet, i])
|
||||
}
|
||||
},
|
||||
|
||||
init(siteNavigation) {
|
||||
slidesData = siteNavigation.map(nav => ({
|
||||
id: nav.id,
|
||||
|
|
@ -56,6 +100,8 @@ export const slides = {
|
|||
if (idx === -1) return
|
||||
if (slidesData[idx].loaded) {
|
||||
activeIndex = idx
|
||||
activeMediaReady = false
|
||||
renderableSet = new Set([...renderableSet, idx])
|
||||
pendingPath = null
|
||||
} else {
|
||||
pendingPath = path
|
||||
|
|
@ -64,6 +110,8 @@ export const slides = {
|
|||
|
||||
setActiveIndex(idx) {
|
||||
activeIndex = idx
|
||||
activeMediaReady = false
|
||||
renderableSet = new Set([...renderableSet, idx])
|
||||
},
|
||||
|
||||
resolvePending() {
|
||||
|
|
@ -71,6 +119,8 @@ export const slides = {
|
|||
const idx = getIndexByPath(pendingPath)
|
||||
if (idx !== -1 && slidesData[idx].loaded) {
|
||||
activeIndex = idx
|
||||
activeMediaReady = false
|
||||
renderableSet = new Set([...renderableSet, idx])
|
||||
pendingPath = null
|
||||
}
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue