Explorer
KNOW-PAT-133

Web — Performance canvas/vidéo : adaptive quality, IntersectionObserver, RAF batching

Domaine
web-uiux
Type
pattern
Priorité
P1

Parent : [[INDEX-WEB-UIUX]]

Web — Performance canvas/vidéo : adaptive quality, IntersectionObserver, RAF batching

Pattern 1 — Canvas animé : détecter les capacités device et adapter

Problème : Un canvas animé complexe freeze sur mobile et Firefox.

Solution : Détecter isMobileDevice(), isLowEndDevice(), et navigator.userAgent pour Firefox. Sur mobile/low-end → fallback CSS gradient statique. Sur Firefox → skip canvas. Ne jamais rendre le canvas avant que le container ait une taille non-nulle.

if (isMobile || isLowEnd) {
  return <div style={{ background: 'linear-gradient(40deg, #02defc, #7ffe00, #fe9efb)' }} />
}

Pattern 2 — Pauser le canvas quand hors viewport (IntersectionObserver)

Problème : Une animation canvas qui tourne hors viewport consomme CPU inutilement.

Solution : Observer le container avec IntersectionObserver (threshold: 0.1). Setter un state isInView. Dans la boucle RAF : si !isInView → skip le calcul des particules mais maintenir le requestAnimationFrame pour ne pas casser la boucle.

const observer = new IntersectionObserver(
  entries => entries.forEach(e => setIsInView(e.isIntersecting)),
  { threshold: 0.1 }
)
observer.observe(containerRef.current)

Pattern 3 — Éviter les forced reflows dans les handlers resize/scroll

Problème : Lire et écrire le DOM dans le même tick (forced reflow) bloque le thread principal.

Solution : Batching via requestAnimationFrame : grouper tous les getBoundingClientRect() (lectures DOM) ensemble, puis tous les writes ensemble dans la même RAF callback. Throttler le resize handler à 250ms.

const throttledResize = throttle(resize, 250)
// Dans resize :
requestAnimationFrame(() => {
  const rect = container.getBoundingClientRect() // lecture groupée
  canvas.width = rect.width * dpr              // écriture groupée
})

Pattern 4 — Float32Array pour les particules

Problème : Stocker les propriétés de particules dans des objets JS génère une GC pressure élevée sur chaque frame.

Solution : Utiliser Float32Array typé avec un stride fixe (PARTICLE_PROPS par particule). Accès direct par index, zéro allocation sur chaque frame.

const PARTICLE_PROPS = 8
particles = new Float32Array(PARTICLE_COUNT * PARTICLE_PROPS)
// x=i, y=i+1, velX=i+2, velY=i+3, life=i+4, ttl=i+5, size=i+6, hue=i+7

Pattern 5 — Video ultra-optimisée : IntersectionObserver + connection detection

Problème : Charger une vidéo 4K 500MB au page load freeze complètement sur mobile ou connexion lente.

Solution : preload="metadata" par défaut. Charger la source uniquement quand isInView (IntersectionObserver). Détecter navigator.connection.effectiveType et navigator.connection.downlink pour downgrader la qualité automatiquement. Pauser quand hors viewport avec pauseWhenHidden.

// Ne charger la vidéo que quand visible
if (shouldLoad && isInView) {
  videoRef.current.src = effectiveQuality === 'low' ? srcLow : srcMp4
}

Pattern 6 — DPR (Device Pixel Ratio) plafonné à 2

Problème : Sur les écrans retina haute densité (DPR 3+), le canvas est 9x plus grand que nécessaire.

Solution : const dpr = Math.min(window.devicePixelRatio || 1, 2) — plafonner à 2 maximum. Appliquer ctx.scale(dpr, dpr) après resize pour que le canvas soit sharp sans être surdimensionné.