Back

Créer des composants sensibles au défilement dans React

Créer des composants sensibles au défilement dans React

Votre gestionnaire de défilement se déclenche 60 fois par seconde. Votre composant se re-rend à chaque tick. Vos utilisateurs voient des saccades au lieu d’animations fluides. Ce pattern casse les applications React plus rapidement que presque toute autre erreur de performance.

Créer des composants sensibles au défilement dans React nécessite de comprendre quand éviter complètement le système d’état de React. Cet article couvre les bons outils pour le suivi de la position de défilement dans React, pourquoi Intersection Observer dans React devrait être votre choix par défaut, et comment implémenter une gestion performante du défilement lorsque vous avez réellement besoin de données brutes de défilement.

Points clés à retenir

  • Intersection Observer gère la plupart des patterns sensibles au défilement sans coût de performance
  • Lorsque vous avez besoin de valeurs de défilement continues, utilisez des refs et requestAnimationFrame au lieu de l’état
  • Réservez useSyncExternalStore pour un état de défilement partagé entre composants
  • Testez avec le routage du framework et fournissez toujours des alternatives avec mouvement réduit

Pourquoi Intersection Observer devrait être votre premier choix

La plupart des patterns d’interface utilisateur React pilotés par le défilement n’ont pas réellement besoin de la position de défilement. Ils ont besoin de détection de visibilité : Cet élément est-il à l’écran ? L’utilisateur a-t-il défilé au-delà de cette section ?

Intersection Observer gère ces cas sans exécuter de code applicatif à chaque événement de défilement. Le navigateur optimise l’observation en interne, ce qui le rend considérablement plus performant que les écouteurs de défilement.

import { useInView } from 'react-intersection-observer';

function FadeInSection({ children }) {
  const { ref, inView } = useInView({
    threshold: 0.1,
    triggerOnce: true,
  });

  return (
    <div ref={ref} className={inView ? 'visible' : 'hidden'}>
      {children}
    </div>
  );
}

La bibliothèque react-intersection-observer encapsule cette API de manière élégante. Utilisez-la pour le suivi de sections, le chargement paresseux et les animations de révélation.

Quand vous avez réellement besoin de la position de défilement

Certains patterns nécessitent des valeurs de défilement continues : effets de parallaxe, indicateurs de progression ou transformations liées au défilement. C’est là que la plupart des tutoriels vous font défaut.

Ne faites jamais ceci :

// Ceci provoque des re-rendus à chaque tick de défilement
const [scrollY, setScrollY] = useState(0);

useEffect(() => {
  const handleScroll = () => setScrollY(window.scrollY);
  window.addEventListener('scroll', handleScroll);
  return () => window.removeEventListener('scroll', handleScroll);
}, []);

Ce pattern déclenche le processus de réconciliation de React plus de 60 fois par seconde pendant un défilement actif.

Le pattern basé sur les refs

Stockez les valeurs de défilement dans des refs et mettez à jour le DOM directement :

import { useRef, useEffect } from 'react';

function ParallaxElement() {
  const elementRef = useRef(null);
  const rafId = useRef(null);

  useEffect(() => {
    const handleScroll = () => {
      if (rafId.current) return;
      
      rafId.current = requestAnimationFrame(() => {
        if (elementRef.current) {
          const offset = window.scrollY * 0.5;
          elementRef.current.style.transform = `translateY(${offset}px)`;
        }
        rafId.current = null;
      });
    };

    window.addEventListener('scroll', handleScroll, { passive: true });
    return () => {
      window.removeEventListener('scroll', handleScroll);
      if (rafId.current) cancelAnimationFrame(rafId.current);
    };
  }, []);

  return <div ref={elementRef}>Contenu parallaxe</div>;
}

Détails importants : requestAnimationFrame limite les mises à jour à la fréquence de rafraîchissement de l’écran. L’option { passive: true } indique au navigateur que vous n’appellerez pas preventDefault(), permettant des optimisations de défilement.

useSyncExternalStore pour un état de défilement partagé

Lorsque plusieurs composants ont besoin de la position de défilement, traitez-la comme un store externe :

import { useSyncExternalStore } from 'react';

const scrollStore = {
  subscribe: (callback) => {
    window.addEventListener('scroll', callback, { passive: true });
    return () => window.removeEventListener('scroll', callback);
  },
  getSnapshot: () => window.scrollY,
  getServerSnapshot: () => 0,
};

function useScrollPosition() {
  return useSyncExternalStore(
    scrollStore.subscribe,
    scrollStore.getSnapshot,
    scrollStore.getServerSnapshot
  );
}

Ce pattern fonctionne correctement avec les fonctionnalités concurrentes de React et gère le rendu côté serveur. En pratique, vous devriez toujours limiter ou regrouper les mises à jour (par exemple en émettant des changements de section plutôt que des pixels bruts) pour éviter de re-rendre à chaque frame de défilement.

useLayoutEffect vs useEffect pour le défilement

Utilisez useLayoutEffect lorsque vous devez mesurer des éléments DOM avant que le navigateur ne peigne. Cela évite le scintillement visuel lors du calcul des positions avec getBoundingClientRect().

Utilisez useEffect pour attacher des écouteurs de défilement. La configuration de l’écouteur n’a pas besoin de bloquer le rendu.

Considérations liées aux frameworks

Next.js App Router et les frameworks similaires gèrent automatiquement la restauration du défilement. Les gestionnaires de défilement personnalisés peuvent entrer en conflit avec ce comportement. Testez les composants sensibles au défilement après une navigation côté client pour détecter les problèmes de restauration.

Animations CSS pilotées par le défilement

Les navigateurs modernes prennent en charge animation-timeline: scroll() pour des effets de défilement en CSS pur. Le support des navigateurs reste incomplet, mais cette approche élimine complètement JavaScript pour les cas supportés. Utilisez-la avec amélioration progressive—fournissez une expérience de repli pour les navigateurs non supportés.

Accessibilité

Respectez toujours prefers-reduced-motion. Encapsulez les animations déclenchées par le défilement :

const prefersReducedMotion =
  typeof window !== 'undefined' &&
  window.matchMedia('(prefers-reduced-motion: reduce)').matches;

Ignorez les animations ou fournissez des transitions instantanées lorsque cela renvoie true.

Conclusion

Les composants sensibles au défilement n’ont pas à ruiner les performances de votre application React. La clé est de choisir le bon outil pour chaque cas d’usage. Privilégiez d’abord Intersection Observer—il gère la détection de visibilité sans surcharge de performance. Lorsque vous avez réellement besoin de valeurs de défilement continues, contournez le système d’état de React en utilisant des refs et requestAnimationFrame pour une manipulation directe du DOM. Pour un état de défilement partagé entre plusieurs composants, useSyncExternalStore fournit un pattern propre et compatible avec le mode concurrent. Quelle que soit l’approche choisie, testez minutieusement avec le routage de votre framework et respectez toujours les préférences utilisateur pour le mouvement réduit.

FAQ

Définir l'état à chaque événement de défilement déclenche le processus de réconciliation de React plus de 60 fois par seconde. Chaque mise à jour d'état provoque un re-rendu, ce qui signifie que React doit comparer le DOM virtuel et potentiellement mettre à jour le DOM réel de manière répétée. Cela surcharge le thread principal et provoque des saccades visibles. L'utilisation de refs contourne cela entièrement car les mises à jour de refs ne déclenchent pas de re-rendus.

Utilisez useSyncExternalStore lorsque plusieurs composants ont besoin de la même valeur de position de défilement. Il s'intègre correctement avec les fonctionnalités de rendu concurrent de React et gère les cas limites comme le tearing, où différents composants pourraient voir des valeurs différentes pendant un rendu. Il fournit également un support intégré pour le rendu côté serveur via getServerSnapshot.

Vous pouvez les utiliser avec amélioration progressive. Les animations CSS pilotées par le défilement fonctionnent actuellement dans Chrome et Edge, avec un support Safari et Firefox encore incomplet ou en cours. Implémentez-les comme une amélioration pour les navigateurs supportés tout en fournissant un repli JavaScript ou une expérience simplifiée pour les autres. Consultez caniuse.com pour le support actuel des navigateurs avant de vous y fier.

Testez après une navigation côté client, pas seulement au chargement initial de la page. L'App Router gère automatiquement la restauration du défilement, ce qui peut entrer en conflit avec les gestionnaires de défilement personnalisés. Naviguez entre les pages en utilisant les composants Link et vérifiez que vos écouteurs de défilement se réattachent correctement et n'interfèrent pas avec le comportement de restauration de position de défilement du framework.

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay