Résolution des à-coups pour améliorer les performances d'affichage

Tom Wiltzius
Tom Wiltzius

Introduction

Vous souhaitez que votre application Web soit réactive et fluide lors des animations, des transitions et d'autres petits effets d'interface utilisateur. S'assurer que ces effets sont fluides peut faire la différence entre une expérience "native" et une expérience maladroite et peu soignée.

Il s'agit du premier d'une série d'articles sur l'optimisation des performances de rendu dans le navigateur. Pour commencer, nous verrons pourquoi il est difficile d'obtenir une animation fluide et ce qu'il faut faire pour y parvenir, ainsi que quelques bonnes pratiques simples. La plupart de ces idées ont été présentées à l'origine dans "Jank Busters", une présentation que Nat Duca et moi-même avons donnée lors de la conférence Google I/O (vidéo) cette année.

Présentation de la synchronisation verticale

Les joueurs sur PC connaissent peut-être ce terme, mais il est peu courant sur le Web : qu'est-ce que la v-sync ?

Prenons l'exemple de l'écran de votre téléphone : il s'actualise à intervalles réguliers, généralement (mais pas toujours) environ 60 fois par seconde. La synchronisation verticale (ou V-sync) consiste à ne générer de nouvelles images qu'entre les actualisations de l'écran. On peut comparer cela à une condition de concurrence entre le processus qui écrit les données dans le tampon d'écran et le système d'exploitation qui lit ces données pour les afficher. Nous voulons que le contenu de l'image mise en mémoire tampon change entre ces actualisations, et non pendant celles-ci. Sinon, le moniteur affichera la moitié d'une image et la moitié d'une autre, ce qui entraînera un déchirement.

Pour obtenir une animation fluide, une nouvelle image doit être prête à chaque actualisation de l'écran. Cela a deux implications majeures : le timing des frames (c'est-à-dire le moment où le frame doit être prêt) et le budget de frame (c'est-à-dire le temps dont dispose le navigateur pour produire un frame). Vous ne disposez que du temps entre les actualisations de l'écran pour terminer une image (environ 16 ms sur un écran 60 Hz), et vous voulez commencer à produire l'image suivante dès que la dernière a été affichée à l'écran.

Le timing a son importance : requestAnimationFrame

De nombreux développeurs Web utilisent setInterval ou setTimeout toutes les 16 millisecondes pour créer des animations. Cela pose problème pour plusieurs raisons (que nous aborderons plus en détail dans un instant), mais les plus préoccupantes sont les suivantes :

  • La résolution du minuteur à partir de JavaScript n'est que de l'ordre de quelques millisecondes
  • La fréquence d'actualisation varie selon les appareils

Rappelez-vous du problème de temps de rendu mentionné ci-dessus : vous avez besoin d'une image d'animation terminée, avec tout le code JavaScript, la manipulation du DOM, la mise en page, l'affichage, etc., pour qu'elle soit prête avant la prochaine actualisation de l'écran. Une faible résolution du minuteur peut rendre difficile l'affichage des images d'animation avant l'actualisation suivante de l'écran, mais la variation des fréquences d'actualisation de l'écran rend cela impossible avec un minuteur fixe. Quel que soit l'intervalle du minuteur, vous allez lentement dériver de la fenêtre de temps d'une image et finir par en perdre une. Cela se produirait même si le minuteur se déclenchait avec une précision d'une milliseconde, ce qui n'est pas le cas (comme les développeurs l'ont découvert). La résolution du minuteur varie selon que l'appareil est branché ou non, et peut être affectée par les onglets en arrière-plan qui monopolisent les ressources, etc. Même si cela est rare (par exemple, toutes les 16 images parce que vous avez un décalage d'une milliseconde), vous le remarquerez : vous perdrez plusieurs images par seconde. Vous allez également générer des images qui ne seront jamais affichées, ce qui gaspille de l'énergie et du temps CPU que vous pourriez consacrer à d'autres tâches dans votre application.

Les différents écrans ont des fréquences d'actualisation différentes : 60 Hz est une fréquence courante, mais certains téléphones sont à 59 Hz, certains ordinateurs portables passent à 50 Hz en mode faible consommation et certains écrans d'ordinateur sont à 70 Hz.

Nous avons tendance à nous concentrer sur le nombre d'images par seconde (FPS) lorsque nous parlons des performances de rendu, mais la variance peut être un problème encore plus important. Nos yeux remarquent les petites irrégularités qu'une animation mal synchronisée peut produire.

Pour obtenir des images d'animation correctement synchronisées, utilisez requestAnimationFrame. Lorsque vous utilisez cette API, vous demandez au navigateur une image d'animation. Votre rappel est appelé lorsque le navigateur est sur le point de produire un nouveau frame. Cela se produit quelle que soit la fréquence d'actualisation.

requestAnimationFrame présente également d'autres propriétés intéressantes :

  • Les animations des onglets en arrière-plan sont mises en pause, ce qui permet d'économiser les ressources système et la batterie.
  • Si le système ne peut pas gérer l'affichage à la fréquence d'actualisation de l'écran, il peut limiter les animations et produire le rappel moins fréquemment (par exemple, 30 fois par seconde sur un écran 60 Hz). Bien que cela réduise de moitié la fréquence d'images, l'animation reste cohérente. Comme indiqué ci-dessus, nos yeux sont beaucoup plus sensibles à la variance qu'à la fréquence d'images. Une fréquence stable de 30 Hz est préférable à une fréquence de 60 Hz qui perd quelques images par seconde.

requestAnimationFrame a déjà été abordé à de nombreuses reprises. Pour en savoir plus, consultez des articles comme celui-ci de Creative JS. Il s'agit d'une première étape importante pour fluidifier l'animation.

Budget de frame

Comme nous voulons qu'une nouvelle image soit prête à chaque actualisation de l'écran, nous ne disposons que du temps entre les actualisations pour effectuer tout le travail de création d'une nouvelle image. Sur un écran 60 Hz, cela signifie que nous disposons d'environ 16 ms pour exécuter l'ensemble du code JavaScript, effectuer la mise en page, l'affichage et tout ce que le navigateur doit faire pour afficher l'image. Cela signifie que si le code JavaScript de votre rappel requestAnimationFrame met plus de 16 ms à s'exécuter, vous n'avez aucune chance de produire une image à temps pour la synchronisation verticale.

16 ms, ce n'est pas beaucoup. Heureusement, les outils pour les développeurs Chrome peuvent vous aider à déterminer si vous dépassez votre budget de frames pendant le rappel requestAnimationFrame.

En ouvrant la chronologie des outils de développement et en enregistrant cette animation en action, on constate rapidement que nous dépassons largement le budget lors de l'animation. Dans la chronologie, passez à "Frames" (Images) et examinez les résultats :

Une démo avec beaucoup trop de mise en page
Une démo avec beaucoup trop de mise en page

Ces rappels requestAnimationFrame (rAF) prennent plus de 200 ms. C'est un ordre de grandeur trop long pour afficher une image toutes les 16 ms. L'ouverture de l'un de ces longs rappels rAF révèle ce qui se passe à l'intérieur : dans ce cas, beaucoup de mise en page.

La vidéo de Paul explique plus en détail la cause spécifique de la réorganisation (la lecture de scrollTop) et comment l'éviter. Mais l'intérêt ici est que vous pouvez examiner le rappel et rechercher ce qui prend autant de temps.

Une démonstration mise à jour avec une mise en page très réduite
Une démo mise à jour avec une mise en page très réduite

Notez les temps de rendu de 16 ms. Cet espace vide dans les frames correspond à la marge de manœuvre dont vous disposez pour effectuer d'autres tâches (ou laisser le navigateur effectuer les tâches qu'il doit effectuer en arrière-plan). Cet espace vide est une bonne chose.

Autre source d'à-coups

La principale cause de problème lorsque vous essayez d'exécuter des animations basées sur JavaScript est que d'autres éléments peuvent interférer avec votre rappel rAF, voire l'empêcher de s'exécuter. Même si votre rappel rAF est léger et s'exécute en quelques millisecondes, d'autres activités (comme le traitement d'un XHR qui vient d'arriver, l'exécution de gestionnaires d'événements d'entrée ou l'exécution de mises à jour planifiées sur un minuteur) peuvent soudainement se produire et s'exécuter pendant une période donnée sans céder. Sur les appareils mobiles, le traitement de ces événements peut parfois prendre des centaines de millisecondes, pendant lesquelles votre animation sera complètement bloquée. Nous appelons ces problèmes d'animation des à-coups.

Il n'existe pas de solution miracle pour éviter ces situations, mais voici quelques bonnes pratiques architecturales pour vous aider à réussir :

  • N'effectuez pas trop de traitements dans les gestionnaires d'entrée. L'utilisation intensive de JS ou la tentative de réorganisation de l'ensemble de la page lors d'un gestionnaire onscroll, par exemple, est une cause très fréquente de saccades importantes.
  • Transférez autant de traitements que possible (c'est-à-dire tout ce qui prendra beaucoup de temps à s'exécuter) vers votre rappel rAF ou vos Web Workers.
  • Si vous transférez du travail dans le rappel rAF, essayez de le diviser afin de ne traiter qu'une petite partie de chaque image ou de le retarder jusqu'à la fin d'une animation importante. De cette façon, vous pouvez continuer à exécuter des rappels rAF courts et à animer de manière fluide.

Pour découvrir comment transférer le traitement vers des rappels requestAnimationFrame plutôt que vers des gestionnaires d'entrée, consultez l'article de Paul Lewis Leaner, Meaner, Faster Animations with requestAnimationFrame.

Animation CSS

Qu'y a-t-il de mieux que du code JS léger dans vos rappels d'événements et rAF ? Pas de JS.

Nous avons dit précédemment qu'il n'existait pas de solution miracle pour éviter d'interrompre vos rappels rAF, mais vous pouvez utiliser l'animation CSS pour éviter d'en avoir besoin. Sur Chrome pour Android en particulier (et d'autres navigateurs travaillent sur des fonctionnalités similaires), les animations CSS ont la propriété très intéressante de pouvoir être exécutées par le navigateur même si JavaScript est en cours d'exécution.

La section ci-dessus sur les à-coups contient une affirmation implicite : les navigateurs ne peuvent faire qu'une seule chose à la fois. Ce n'est pas tout à fait vrai, mais c'est une bonne hypothèse de travail : à tout moment, le navigateur peut exécuter du code JS, effectuer une mise en page ou peindre, mais une seule de ces opérations à la fois. Vous pouvez le vérifier dans la vue "Chronologie" des outils de développement. Les animations CSS sur Chrome pour Android (et bientôt sur Chrome pour ordinateur, mais pas encore) constituent l'une des exceptions à cette règle.

Dans la mesure du possible, l'utilisation d'une animation CSS simplifie votre application et permet aux animations de s'exécuter de manière fluide, même lorsque JavaScript est en cours d'exécution.

  // see http://paulirish.com/2011/requestanimationframe-for-smart-animating/ for info on rAF polyfills
  rAF = window.requestAnimationFrame;

  var degrees = 0;
  function update(timestamp) {
    document.querySelector('#foo').style.webkitTransform = "rotate(" + degrees + "deg)";
    console.log('updated to degrees ' + degrees);
    degrees = degrees + 1;
    rAF(update);
  }
  rAF(update);

Si vous cliquez sur le bouton, le code JavaScript s'exécute pendant 180 ms, ce qui entraîne des à-coups. En revanche, si nous pilotons cette animation avec des animations CSS, les à-coups ne se produisent plus.

(N'oubliez pas qu'au moment de la rédaction de cet article, l'animation CSS n'est fluide que sur Chrome pour Android, et non sur Chrome pour ordinateur.)

  /* tools like Modernizr (http://modernizr.com/) can help with CSS polyfills */
  #foo {
    +animation-duration: 3s;
    +animation-timing-function: linear;
    +animation-animation-iteration-count: infinite;
    +animation-animation-name: rotate;
  }

  @+keyframes: rotate; {
    from {
      +transform: rotate(0deg);
    }
    to {
      +transform: rotate(360deg);
    }
  }

Pour en savoir plus sur l'utilisation des animations CSS, consultez des articles comme celui-ci sur MDN.

Conclusion

En résumé :

  1. Lors de l'animation, il est important de produire des images pour chaque actualisation de l'écran. L'animation synchronisée verticalement a un impact positif considérable sur l'expérience utilisateur d'une application.
  2. La meilleure façon d'obtenir une animation synchronisée verticalement dans Chrome et d'autres navigateurs récents est d'utiliser l'animation CSS. Lorsque vous avez besoin de plus de flexibilité que l'animation CSS, la meilleure technique est l'animation basée sur requestAnimationFrame.
  3. Pour que les animations rAF fonctionnent correctement, assurez-vous que les autres gestionnaires d'événements n'interfèrent pas avec l'exécution de votre rappel rAF et que les rappels rAF sont courts (< 15 ms).

Enfin, l'animation synchronisée avec la synchronisation verticale ne s'applique pas seulement aux animations d'interface utilisateur simples, mais aussi aux animations Canvas2D, aux animations WebGL et même au défilement sur les pages statiques. Dans le prochain article de cette série, nous examinerons les performances de défilement en gardant ces concepts à l'esprit.

Bonne animation !

Références