perf(reader): windowing + eager loading sur l'InfiniteReader
- Windowing côté rendu : seules les pages dans une fenêtre de ±3 autour de la page visible sont montées en tant que ReaderPage ; les autres sont remplacées par des placeholders dimensionnés via aspect-ratio CSS pour maintenir la hauteur de scroll sans saut - IntersectionObserver utilise le minimum des indices intersectants pour éviter que les entrées simultanées au chargement ne décalent la fenêtre - Prop initialPage passé depuis ChapterReader pour ancrer la fenêtre sur la page courante dès le montage - loading="eager" sur les ReaderPage montés (le windowing est le mécanisme de lazy-loading, pas l'attribut HTML natif) - Prop loading bindé sur les 3 balises <img> de ReaderPage
This commit is contained in:
parent
be8a3c6de8
commit
74f033f5d1
@@ -1,10 +1,15 @@
|
||||
<template>
|
||||
<div class="infinite-reader" ref="containerRef">
|
||||
<div v-for="(page, index) in pages" :key="index" class="page-wrapper">
|
||||
<div v-if="!page?.url" class="loading">
|
||||
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
|
||||
</div>
|
||||
<ReaderPage v-else :page-data="page" :page-number="index + 1" :zoom="zoom" :double-page-mode="doublePageMode" loading="lazy" />
|
||||
<div v-for="(page, index) in pages" :key="index" class="page-wrapper" :data-page-index="index">
|
||||
<ReaderPage
|
||||
v-if="isPageInWindow(index) && page?.url"
|
||||
:page-data="page"
|
||||
:page-number="index + 1"
|
||||
:zoom="zoom"
|
||||
:double-page-mode="doublePageMode"
|
||||
loading="eager"
|
||||
/>
|
||||
<div v-else class="page-placeholder" :style="getPlaceholderStyle(page)" />
|
||||
</div>
|
||||
|
||||
<!-- Bouton flottant pour revenir en haut -->
|
||||
@@ -37,6 +42,25 @@
|
||||
import { useHeaderStore } from '../../../../shared/stores/headerStore';
|
||||
import ReaderPage from './ReaderPage.vue';
|
||||
|
||||
const WINDOW_SIZE = 3;
|
||||
const currentVisibleIndex = ref(0); // initialisé via prop initialPage dans onMounted
|
||||
|
||||
const isPageInWindow = (index) => Math.abs(index - currentVisibleIndex.value) <= WINDOW_SIZE;
|
||||
|
||||
const getPlaceholderStyle = (page) => {
|
||||
if (page?.dimensions?.width && page?.dimensions?.height) {
|
||||
const maxW = windowWidth.value < 1200
|
||||
? windowWidth.value * 0.95
|
||||
: 1200;
|
||||
return {
|
||||
aspectRatio: `${page.dimensions.width} / ${page.dimensions.height}`,
|
||||
width: '100%',
|
||||
maxWidth: `${Math.min(page.dimensions.width, maxW)}px`,
|
||||
};
|
||||
}
|
||||
return { height: '800px', width: '100%' };
|
||||
};
|
||||
|
||||
const props = defineProps({
|
||||
pages: {
|
||||
type: Array,
|
||||
@@ -49,6 +73,10 @@ import ReaderPage from './ReaderPage.vue';
|
||||
doublePageMode: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
initialPage: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
});
|
||||
|
||||
@@ -68,12 +96,15 @@ import ReaderPage from './ReaderPage.vue';
|
||||
let scrollDirection = 'down';
|
||||
|
||||
const observeIntersection = entries => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
const pageIndex = parseInt(entry.target.getAttribute('data-page-index'));
|
||||
emit('pageVisible', pageIndex);
|
||||
}
|
||||
});
|
||||
const intersectingIndices = entries
|
||||
.filter(e => e.isIntersecting)
|
||||
.map(e => parseInt(e.target.getAttribute('data-page-index')));
|
||||
|
||||
if (intersectingIndices.length > 0) {
|
||||
const minIdx = Math.min(...intersectingIndices);
|
||||
currentVisibleIndex.value = minIdx;
|
||||
emit('pageVisible', minIdx);
|
||||
}
|
||||
};
|
||||
|
||||
const setupIntersectionObserver = () => {
|
||||
@@ -89,8 +120,7 @@ import ReaderPage from './ReaderPage.vue';
|
||||
nextTick(() => {
|
||||
const pageElements = containerRef.value?.querySelectorAll('.page-wrapper');
|
||||
if (pageElements) {
|
||||
pageElements.forEach((element, index) => {
|
||||
element.setAttribute('data-page-index', index);
|
||||
pageElements.forEach(element => {
|
||||
observer.value.observe(element);
|
||||
});
|
||||
}
|
||||
@@ -247,6 +277,7 @@ import ReaderPage from './ReaderPage.vue';
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
currentVisibleIndex.value = props.initialPage;
|
||||
setupIntersectionObserver();
|
||||
|
||||
// Activer l'auto-hide du header si la largeur < 1200px
|
||||
@@ -303,29 +334,26 @@ import ReaderPage from './ReaderPage.vue';
|
||||
@apply mb-2 sm:mb-4 px-1 sm:px-4;
|
||||
}
|
||||
|
||||
.loading,
|
||||
.page-placeholder {
|
||||
@apply flex justify-center;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.error {
|
||||
@apply flex items-center justify-center min-h-[400px];
|
||||
/* Largeur adaptative selon la taille d'écran */
|
||||
width: 95vw; /* Mobile : 95% de la largeur */
|
||||
@apply text-red-500 text-xl bg-red-500/10 rounded-lg flex items-center justify-center min-h-[400px];
|
||||
width: 95vw;
|
||||
}
|
||||
|
||||
@screen sm {
|
||||
.loading,
|
||||
.error {
|
||||
width: 80vw; /* Tablette : 80% de la largeur */
|
||||
width: 80vw;
|
||||
}
|
||||
}
|
||||
|
||||
@screen lg {
|
||||
.loading,
|
||||
.error {
|
||||
width: 70vw; /* Desktop : 70% de la largeur */
|
||||
width: 70vw;
|
||||
}
|
||||
}
|
||||
|
||||
.error {
|
||||
@apply text-red-500 text-xl bg-red-500/10 rounded-lg;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user