Avez-vous déjà conçu une infobulle qui saute du coin supérieur gauche à sa position correcte ? Ou une fenêtre modale qui s'affiche à la mauvaise taille avant de se stabiliser ? Ce glitch d'une fraction de seconde est un scintillement de mise en page (layout flicker). Cela se produit lorsque React lit le DOM, calcule une correction et met à jour l'état, mais que le navigateur a déjà commencé à afficher les pixels à l'écran. La prescription habituelle est de remplacer useEffect par useLayoutEffect. Ce remplacement fonctionne, mais seulement si vous comprenez exactement quand chaque hook s'exécute dans le pipeline du navigateur.
Le pipeline du navigateur : Render, Commit, Paint
React met à jour un composant en trois étapes distinctes. Lors de la phase de render, React construit — ou reconstruit — le Virtual DOM et calcule le diff. Il n'y a pas encore de changements de pixels réels ; il s'agit d'un pur calcul effectué en mémoire. Vient ensuite la phase de commit, où React applique ces changements aux nœuds du DOM réel. Les styles sont mis à jour, des nœuds sont insérés ou supprimés, et le texte change.
Ensuite, le navigateur prend le relais. Lors de la phase de paint, le moteur de rendu du navigateur calcule la géométrie de la mise en page et dessine les pixels sur l'écran. Cette séquence est rigide. Le navigateur doit terminer la mise en page (layout) avant de pouvoir peindre (paint), et il doit terminer la peinture avant que l'utilisateur ne voie quoi que ce soit de nouveau. L'écart entre le commit et le paint se mesure en millisecondes, mais il est bien réel, et c'est dans cette fenêtre que useEffect et useLayoutEffect divergent.
Pourquoi useEffect provoque le scintillement
useEffect s'exécute de manière asynchrone, programmé pour se déclencher après que le navigateur a déjà peint l'écran. Le DOM est mis à jour, les pixels sont dessinés, puis React intervient pour exécuter votre effet.
Imaginez que vous affichiez un menu déroulant sous un bouton. À l'intérieur de useEffect, vous appelez buttonRef.current.getBoundingClientRect(), calculez les coordonnées top et left correctes, et les stockez dans l'état (state). Comme useEffect s'exécute après le paint, le navigateur a déjà dessiné le menu déroulant à sa position par défaut, peut-être à top: 0, left: 0. Ce n'est qu'après ce paint que votre effet met à jour l'état. React effectue un commit des coordonnées corrigées, et le navigateur peint à nouveau. L'utilisateur voit deux images : la mauvaise position, puis la bonne. Ce saut visuel est le scintillement que tout le monde essaie d'éviter.
Pour la récupération de données, les appels API, le suivi analytique ou la mise en place d'écouteurs d'événements, ce délai n'a pas d'importance. L'utilisateur ne se soucie pas de savoir si un signal analytique est envoyé quelques millisecondes après le paint. En fait, reporter le travail non visuel après le paint permet de maintenir la réactivité du rendu initial. Mais pour les corrections dépendantes de la mise en page, useEffect arrive tout simplement trop tard.
Comment useLayoutEffect bloque le paint
useLayoutEffect s'exécute de manière synchrone, immédiatement après que React a muté le DOM, mais avant que le navigateur n'ait la chance de calculer la mise en page ou de peindre les pixels. Il bloque entièrement le pipeline de peinture.
Si vous effectuez la même mesure de menu déroulant à l'intérieur de useLayoutEffect, la séquence change. React effectue le commit de la mise à jour initiale du DOM, exécute votre effet de mise en page, et votre mise à jour d'état déclenche un re-render synchrone. React effectue le commit des coordonnées corrigées, et ce n'est qu'ensuite que le navigateur peint. L'utilisateur ne voit qu'une seule image, déjà correcte.
Ce comportement bloquant est à la fois un atout et un risque. Parce que useLayoutEffect empêche le navigateur de peindre tant qu'il n'a pas terminé, tout calcul lourd à l'intérieur de celui-ci fige l'interface utilisateur. Même quelques dizaines de millisecondes de blocage du paint sont ressenties comme des saccades (jank) par l'utilisateur. C'est pourquoi la documentation de React vous conseille explicitement de commencer avec useEffect et de ne passer à useLayoutEffect que lorsque vous observez réellement un scintillement que vous ne pouvez tolérer.
Quand utiliser chaque hook
La majeure partie de votre logique doit se trouver dans useEffect. Utilisez-le pour :
- Récupérer des données depuis une API
- Mettre en place des abonnements ou des écouteurs d'événements
- Envoyer des événements analytiques
- Tout effet secondaire qui ne lit pas ou ne modifie pas immédiatement la mise en page (layout)
Réservez useLayoutEffect aux opérations qui doivent lire le DOM et réécrire des données avant que l'utilisateur ne voie l'image :
- Mesurer les dimensions d'un élément, comme la largeur, la hauteur ou la position de défilement (scroll)
- Calculer les coordonnées pour des infobulles, des popovers ou des menus contextuels
- Empêcher les décalages de mise en page visibles lorsque la position visuelle dépend de la géométrie rendue
Si vous hésitez sur le choix à faire, optez par défaut pour useEffect. Ne passez à useLayoutEffect que lorsque vous remarquez une instabilité visuelle. Cette règle à elle seule permettra à la grande majorité des applications React de fonctionner de manière fluide.
Le piège du rendu côté serveur (SSR)
Si vous utilisez Next.js, Remix ou n'importe quel framework qui rend React côté serveur, vous rencontrerez un avertissement avec useLayoutEffect. Comme le serveur n'a pas de DOM, le hook n'a rien à mesurer. React vous avertit qu'il s'attendait à un environnement de navigateur et qu'il ne l'a pas trouvé. Lors de l'hydratation, ce décalage peut également causer des bugs subtils car le marquage rendu par le serveur et le premier rendu prévu par le client peuvent différer.
La solution standard est un hook isomorphe qui sélectionne le bon effet en fonction de l'environnement :
const useIsomorphicLayoutEffect =
typeof window !== 'undefined' ? useLayoutEffect : useEffect;
Utilisez ce wrapper dans n'importe quel composant qui doit mesurer des nœuds DOM mais qui pourrait s'exécuter pendant le rendu côté serveur. Il neutralise l'avertissement et maintient la cohérence de votre sortie serveur.
Performance et bonnes pratiques
Puisque useLayoutEffect bloque le rendu (paint), gardez le corps du hook aussi léger que possible. Lisez la valeur de la mise en page, calculez la correction et réécrivez-la. Ne récupérez pas de données, ne parsez pas de gros objets et n'exécutez pas d'algorithmes coûteux à l'intérieur. Un code lourd ici bloquera le thread principal et donnera l'impression que votre interface est figée.
Lorsque vous mesurez des éléments, utilisez des refs React plutôt que document.getElementById. Les refs sont liées à l'instance de votre composant, survivent aux re-rendus sans astuces de requête et fonctionnent de manière fiable avec les portails ou le rendu conditionnel. Les recherches d'ID globales brisent l'encapsulation des composants et peuvent retourner null précisément au moment où vous en avez besoin.
useEffect est la valeur par défaut appropriée pour presque tous les effets de bord. Il permet au navigateur de peindre sans interruption et gère proprement les données, les événements et la synchronisation externe. useLayoutEffect est un outil spécialisé pour un problème spécifique : lire la mise en page et réécrire avant le rendu (paint). Maîtrisez la différence de timing entre les deux, et vous cesserez de courir après les clignotements pour commencer à les prévenir.
L'essentiel à retenir : Commencez par useEffect pour tout. Dès l'instant où vous voyez une infobulle ou une modale clignoter au mauvais endroit avant de se corriger, c'est votre signal. Passez à useLayoutEffect, mesurez le DOM, ajustez votre mise en page et laissez le navigateur peindre une seule fois — correctement.
