perf(reader): virtual rendering avec IntersectionObserver en mode scroll

Remplace le rendu de tous les composants ReaderPage par un système de
virtual rendering : seules les pages dans la zone ±1000px du viewport
sont montées, les autres sont remplacées par un placeholder dimensionné.

- InfiniteReader : ajout visibilityObserver + mountedPageIndices (Set
  réactif), helper getPlaceholderHeight(), suppression de 5 console.log
- ReaderPage : prop windowWidth injectable depuis le parent, listener
  resize conditionnel, suppression de 3 console.log de debug
This commit is contained in:
ext.jeremy.guillot@maxicoffee.domains
2026-03-15 18:44:51 +01:00
parent c268b2c312
commit aba8e36231
2 changed files with 97 additions and 93 deletions

View File

@@ -15,7 +15,6 @@
:alt="`Page ${pageNumber} (Double page)`"
class="page-image rotated"
:style="doublePageRotatedStyle"
:loading="loading"
@load="handleImageLoad"
ref="imageRef" />
<div class="rotation-hint">
@@ -34,7 +33,6 @@
:alt="`Page ${pageNumber} (Double page)`"
class="page-image scrollable"
:style="doublePageScrollStyle"
:loading="loading"
@load="handleImageLoad"
ref="imageRef" />
</div>
@@ -54,7 +52,6 @@
:alt="`Page ${pageNumber}`"
class="page-image"
:style="imageStyle"
:loading="loading"
@load="handleImageLoad"
ref="imageRef" />
</div>
@@ -82,9 +79,9 @@ import { useReaderStore } from '../../application/store/readerStore';
default: 'rotate', // 'rotate', 'scroll', 'normal'
validator: (value) => ['rotate', 'scroll', 'normal'].includes(value)
},
loading: {
type: String,
default: 'lazy',
windowWidth: {
type: Number,
default: null
}
});
@@ -103,8 +100,11 @@ import { useReaderStore } from '../../application/store/readerStore';
const scrollContainerRef = ref(null);
const naturalWidth = ref(0);
const naturalHeight = ref(0);
const windowWidth = ref(window.innerWidth);
const isMobile = computed(() => windowWidth.value < 768);
const localWindowWidth = ref(window.innerWidth);
const effectiveWindowWidth = computed(() =>
props.windowWidth !== null ? props.windowWidth : localWindowWidth.value
);
const isMobile = computed(() => effectiveWindowWidth.value < 768);
const imageLoaded = ref(false);
const imageSource = computed(() => {
@@ -123,17 +123,13 @@ import { useReaderStore } from '../../application/store/readerStore';
// Utiliser d'abord les dimensions de l'API si disponibles
if (props.pageData?.dimensions?.width && props.pageData?.dimensions?.height) {
const ratio = props.pageData.dimensions.width / props.pageData.dimensions.height;
const isDouble = ratio > threshold;
console.log(`API Dimensions - Page ${props.pageNumber}: ${props.pageData.dimensions.width}x${props.pageData.dimensions.height}, ratio: ${ratio.toFixed(2)}, isDouble: ${isDouble}`);
return isDouble;
return ratio > threshold;
}
// Fallback sur les dimensions naturelles de l'image (seulement si l'image est chargée)
if (imageLoaded.value && naturalWidth.value && naturalHeight.value) {
const ratio = naturalWidth.value / naturalHeight.value;
const isDouble = ratio > threshold;
console.log(`Natural Dimensions - Page ${props.pageNumber}: ${naturalWidth.value}x${naturalHeight.value}, ratio: ${ratio.toFixed(2)}, isDouble: ${isDouble}`);
return isDouble;
return ratio > threshold;
}
return false;
@@ -144,7 +140,6 @@ import { useReaderStore } from '../../application/store/readerStore';
naturalWidth.value = imageRef.value.naturalWidth;
naturalHeight.value = imageRef.value.naturalHeight;
imageLoaded.value = true;
console.log(`Image loaded - Page ${props.pageNumber}: ${naturalWidth.value}x${naturalHeight.value}`);
// Positionner le scroll à droite si c'est le mode scroll
if (props.doublePageMode === 'scroll' && scrollContainerRef.value) {
@@ -195,7 +190,7 @@ import { useReaderStore } from '../../application/store/readerStore';
if (!width || !height) return null;
const availableWidth = windowWidth.value;
const availableWidth = effectiveWindowWidth.value;
// Si la largeur disponible est < 1200px : utiliser 95% de la largeur
if (availableWidth < 1200) {
@@ -244,7 +239,7 @@ import { useReaderStore } from '../../application/store/readerStore';
if (!width || !height) return {};
// En mode rotation : maximiser l'utilisation de l'espace
const availableWidth = windowWidth.value;
const availableWidth = effectiveWindowWidth.value;
const availableHeight = window.innerHeight - 100; // Laisser un peu d'espace pour les contrôles
// Après rotation, la largeur originale devient la hauteur affichée
@@ -294,20 +289,18 @@ import { useReaderStore } from '../../application/store/readerStore';
};
});
// Gestion du redimensionnement de la fenêtre
const handleResize = () => {
windowWidth.value = window.innerWidth;
};
let ownResizeHandler = null;
onMounted(() => {
if (imageRef.value && imageRef.value.complete) {
handleImageLoad();
if (props.windowWidth === null) {
ownResizeHandler = () => { localWindowWidth.value = window.innerWidth; };
window.addEventListener('resize', ownResizeHandler, { passive: true });
}
window.addEventListener('resize', handleResize);
if (imageRef.value?.complete) handleImageLoad();
});
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
if (ownResizeHandler) window.removeEventListener('resize', ownResizeHandler);
});
</script>