</> HTML5Advent
ENFRESDEITPT

// apis

ResizeObserver, c'est quoi ? Le redimensionnement au niveau de l'élément, et l'erreur de boucle que tout le monde rencontre

L'événement resize de window ne dit rien d'un élément précis. ResizeObserver, si. Ce qu'il rapporte, la différence entre content box et border box, et pourquoi « ResizeObserver loop completed with undelivered notifications » apparaît dans votre console.

Un mètre ruban jaune tendu en diagonale sur fond gris, graduations de 21 à 42 avec le 30 et le 40 imprimés en rouge

L'événement resize sur window se déclenche quand la fenêtre d'affichage change. C'est tout ce qu'il sait. Un élément peut être redimensionné par un conteneur flex, par l'ouverture d'une barre latérale, par une police qui finit par se charger ou par un textarea qu'on étire : window n'en saura jamais rien.

ResizeObserver est l'API qui surveille un élément plutôt que la fenêtre.

Ce qu'il rapporte réellement

MDN le formule précisément : l'interface « rapporte les changements de dimensions de la content box ou de la border box d'un Element, ou de la bounding box d'un SVGElement ».

Ces deux boîtes ne sont pas la même chose, et se tromper de l'une pour l'autre est une source classique de bugs à quelques pixels près. La content box est l'endroit où va le contenu : la border box moins le padding et la bordure. La border box englobe ensemble le contenu, le padding et la bordure.

const observer = new ResizeObserver((entries) =&gt; {
  for (const entry of entries) {
    const { width, height } = entry.contentRect;
    entry.target.classList.toggle('is-narrow', width &lt; 400);
  }
});

observer.observe(document.querySelector('.card'));

Le callback reçoit un tableau d'entrées, parce qu'un même observateur peut surveiller plusieurs éléments et que plusieurs d'entre eux peuvent changer dans la même frame. Lire entry.contentRect donne la content box ; entry.borderBoxSize et entry.contentBoxSize donnent les boîtes sous forme de tableaux d'objets de dimensions.

L'erreur que tout le monde croise : « loop completed with undelivered notifications »

Tôt ou tard, elle apparaît dans la console, et elle mérite d'être comprise plutôt que réduite au silence.

Le problème dont elle protège est circulaire : votre callback change la taille de quelque chose, ce changement redéclenche l'observateur, qui change de nouveau la taille. Livrée à elle-même, la page ne se stabiliserait jamais.

// Boucle: le callback change la taille de ce qu il observe
const bad = new ResizeObserver(([entry]) =&gt; {
  entry.target.style.height = entry.contentRect.height + 1 + 'px';
});

La réponse de la spécification, dans les termes de MDN, est que « les boucles infinies dues à des dépendances cycliques sont traitées en ne traitant, à chaque itération, que les éléments plus profonds dans le DOM ». Tout ce qui ne remplit pas cette condition est reporté au prochain rendu, et un événement d'erreur est déclenché sur window avec un message fixe :

ResizeObserver loop completed with undelivered notifications.

Une personne se photographiant dans un miroir de salle de bain faisant face à un autre miroir, si bien que le reflet se répète en s'éloignant dans des cadres de plus en plus petits, la scène entière teintée de bleu-vert

Voici la partie qui compte le plus, et MDN l'énonce sans détour : « cela empêche seulement le figeage du navigateur, pas la boucle infinie elle-même ». Le navigateur se protège, il ne corrige pas votre code. Le message est un symptôme, et la boucle est toujours là.

Vous verrez peut-être aussi ResizeObserver loop limit exceeded, une ancienne formulation de Chrome pour la même famille de problème. Chaîne différente, même chose à corriger.

Comment cesser de la provoquer

N'écrivez, dans le callback, rien qui change la taille de ce que vous observez. Basculer une classe qui ne change qu'une couleur est sans risque. Fixer une hauteur à partir d'une hauteur mesurée est la boucle des manuels.

Observez le parent, stylez l'enfant. Puisque l'algorithme traite d'abord les éléments plus profonds dans le DOM, agir vers le bas plutôt que vers le haut vous maintient du bon côté.

Différez l'écriture. Envelopper la mutation dans requestAnimationFrame la sort de la passe d'observation. À considérer comme un contournement : si la dépendance est réellement circulaire, vous l'avez repoussée, pas supprimée.

Quand vous n'en avez pas besoin du tout

Si vous voulez seulement styler un composant selon sa propre largeur, les container queries CSS le font en CSS, sans callback ni boucle à surveiller. Réservez ResizeObserver aux cas où vous avez besoin du nombre lui-même en JavaScript : redessiner un canvas, disposer un graphique, recalculer ce que CSS ne sait pas exprimer.

Et si vous guettez l'entrée d'un élément dans la fenêtre d'affichage plutôt qu'un changement de taille, c'est un tout autre observateur : voir l'API Intersection Observer.

En résumé

ResizeObserver est la réponse au niveau de l'élément à un événement au niveau de la fenêtre. Choisissez la bonne boîte, ne redimensionnez jamais depuis le callback ce que vous observez, et lisez l'erreur de boucle comme un vrai signal plutôt que comme du bruit de console : le navigateur s'est seulement empêché de figer, il n'a pas corrigé le cycle.