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

Tom Wiltzius
Tom Wiltzius

Introduction

Vous voulez 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 allons expliquer pourquoi l'animation fluide est difficile à réaliser, ce qui doit être fait pour y parvenir, ainsi que quelques bonnes pratiques simples. Beaucoup de ces idées ont été présentées à l'origine dans "Jank Busters", une conférence que Nat Duca et moi-même avons donnée lors de Google I/O cette année (vidéo).

Présentation de la synchronisation verticale

Les joueurs sur PC connaissent peut-être ce terme, mais il est peu courant sur le Web. De quoi s'agit-il ? V-sync

Prenons 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 consiste à générer de nouvelles images uniquement entre les actualisations d'écran. Vous pouvez considérer cela comme une condition de course entre le processus qui écrit des 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 des frames mis en mémoire tampon change entre ces actualisations, et non pendant celles-ci. Sinon, l'écran affichera la moitié d'un frame et la moitié d'un autre, ce qui entraînera un découpage.

Pour obtenir une animation fluide, vous devez préparer un nouveau frame chaque fois qu'un écran est actualisé. Cela a deux grandes implications: le timing des frames (c'est-à-dire le moment où le frame doit être prêt) et le budget des frames (c'est-à-dire le temps dont le navigateur dispose pour produire un frame). Vous n'avez que le temps entre les actualisations de l'écran pour terminer un frame (environ 16 ms sur un écran 60 Hz), et vous souhaitez commencer à produire le frame suivant dès que le dernier a été affiché à l'écran.

Le timing est essentiel: 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 (nous en parlerons plus en détail dans une minute), mais les problèmes suivants sont particulièrement préoccupants:

  • La résolution du minuteur à partir de JavaScript n'est que de l'ordre de quelques millisecondes.
  • La fréquence d'actualisation varie d'un appareil à l'autre

Rappelez-vous le problème de synchronisation des images mentionné ci-dessus: vous devez disposer d'un frame d'animation terminé, avec tout JavaScript, manipulation DOM, mise en page, peinture, etc., pour qu'il soit prêt avant le prochain rafraîchissement de l'écran. Une résolution de minuteur faible peut rendre difficile l'achèvement des images d'animation avant la prochaine actualisation 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 progressivement sortir de la fenêtre de synchronisation d'un frame et en perdre un. Cela se produirait même si le minuteur se déclenchait avec une précision de milliseconde, ce qui n'est pas le cas (comme l'ont découvert les développeurs). La résolution du minuteur varie selon que l'ordinateur fonctionne sur batterie ou sur secteur, peut être affectée par les onglets en arrière-plan qui monopolisent les ressources, etc. Même si cela est rare (par exemple, tous les 16 frames, car vous avez été décalé d'une milliseconde), vous remarquerez que vous perdrez plusieurs images par seconde. Vous devrez également générer des frames qui ne s'affichent jamais, ce qui gaspille de l'énergie et du temps de processeur que vous pourriez consacrer à d'autres tâches dans votre application.

Les fréquences d'actualisation des écrans sont 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 économie d'énergie, et certains écrans d'ordinateurs de bureau sont à 70 Hz.

Nous avons tendance à nous concentrer sur les 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 petits à-coups irréguliers dans l'animation 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 un frame d'animation. Votre rappel est appelé lorsque le navigateur va bientôt générer un nouveau frame. Cela se produit quelle que soit la fréquence d'actualisation.

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

  • Les animations des onglets en arrière-plan sont mises en pause, ce qui permet de préserver les ressources système et l'autonomie de la batterie.
  • Si le système ne peut pas gérer le rendu à la fréquence d'actualisation de l'écran, il peut limiter les animations et générer 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 de 30 Hz stable est plus agréable qu'une fréquence de 60 Hz qui manque quelques images par seconde.

requestAnimationFrame a déjà été abordé dans de nombreux articles. Pour en savoir plus, consultez cet article de Creative JS. Il s'agit d'une première étape importante pour fluidifier l'animation.

Budget de frames

Étant donné que nous souhaitons qu'un nouveau frame soit prêt à chaque actualisation de l'écran, nous n'avons que le temps entre les actualisations pour effectuer tout le travail de création d'un nouveau frame. Sur un écran 60 Hz, cela signifie que nous avons environ 16 ms pour exécuter tout le code JavaScript, effectuer la mise en page, la peinture et tout ce que le navigateur doit faire pour afficher le frame. Cela signifie que si l'exécution du code JavaScript dans le rappel requestAnimationFrame prend plus de 16 ms, vous n'avez aucune chance de produire un frame à temps pour la synchronisation verticale.

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

En ouvrant la timeline des outils de développement et en enregistrant cette animation en action, nous constatons rapidement que nous dépassons largement le budget pour l'animation. Dans la chronologie, passez à "Cadres" et regardez:

Une démonstration avec beaucoup trop de mise en page
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 un frame 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 redisposition (elle lit scrollTop) et comment l'éviter. Mais l'essentiel est que vous pouvez examiner le rappel et déterminer pourquoi il prend autant de temps.

Démo mise à jour avec une mise en page beaucoup plus réduite
Démo mise à jour avec une mise en page beaucoup plus réduite

Notez les temps de frame de 16 ms. Cet espace vide dans les cadres est l'espace dont vous disposez pour effectuer plus de travail (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 de problèmes d'à-coups

Le plus grand problème lors de l'exécution d'animations JavaScript est que d'autres éléments peuvent gêner votre rappel rAF et même 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 s'exécuter soudainement et pendant n'importe quelle période sans céder. Sur les appareils mobiles, le traitement de ces événements peut parfois prendre des centaines de millisecondes, pendant lesquelles votre animation est complètement bloquée. Nous appelons ces à-coups d'animation des à-coups.

Il n'existe pas de solution miracle pour éviter ces situations, mais voici quelques bonnes pratiques d'architecture pour vous donner toutes les chances de réussir:

  • N'effectuez pas trop de traitement dans les gestionnaires d'entrée. L'exécution de beaucoup de code JavaScript ou la tentative de réorganiser l'intégralité de la page lors d'un gestionnaire onscroll, par exemple, est une cause très courante de problèmes de saccades.
  • Transférez autant de traitement (c'est-à-dire tout ce qui prendra beaucoup de temps à s'exécuter) que possible dans votre rappel rAF ou vos Web Workers.
  • Si vous transmettez du travail dans le rappel rAF, essayez de le diviser en plusieurs parties afin de ne traiter qu'une petite partie à chaque frame ou de le retarder jusqu'à la fin d'une animation importante. Vous pourrez ainsi continuer à exécuter des rappels rAF courts et à animer de manière fluide.

Pour découvrir comment transférer le traitement dans les rappels requestAnimationFrame plutôt que dans les gestionnaires d'entrée, consultez l'article de Paul Lewis Animations plus efficaces et plus rapides avec requestAnimationFrame.

Animation CSS

Que peut-on faire de mieux que du code JavaScript léger dans vos rappels d'événement et de rAF ? Pas de code JavaScript.

Nous avons précédemment indiqué qu'il n'existe pas de solution miracle pour éviter d'interrompre vos rappels rAF, mais vous pouvez utiliser l'animation CSS pour vous en passer complètement. 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 que le navigateur peut souvent les exécuter même si JavaScript est en cours d'exécution.

La section ci-dessus sur le "jank" contient une déclaration implicite: les navigateurs ne peuvent effectuer qu'une seule action à 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 JavaScript, effectuer une mise en page ou effectuer une peinture, mais une seule à la fois. Vous pouvez le vérifier dans la vue "Chronologie" des outils pour les développeurs. Les animations CSS sur Chrome pour Android (et bientôt Chrome pour ordinateur, mais pas encore) constituent une exception à cette règle.

Lorsque c'est possible, utilisez une animation CSS pour simplifier votre application et permettre aux animations de s'exécuter correctement, même lorsque JavaScript s'exécute.

  // 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, JavaScript s'exécute pendant 180 ms, ce qui provoque un à-coup. Toutefois, si nous pilotons cette animation avec des animations CSS, le problème de saccade ne se produit plus.

(N'oubliez pas qu'au moment de la rédaction de cet article, l'animation CSS n'est fluide que dans Chrome pour Android, et non dans 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 avec Vsync a un impact positif énorme sur l'expérience utilisateur d'une application.
  2. Le meilleur moyen d'obtenir une animation vsync dans Chrome et d'autres navigateurs modernes est d'utiliser une animation CSS. Lorsque vous avez besoin de plus de flexibilité que l'animation CSS, la meilleure technique consiste à utiliser l'animation basée sur requestAnimationFrame.
  3. Pour que les animations rAF restent saines et agréables, assurez-vous que les autres gestionnaires d'événements n'entravent pas l'exécution de votre rappel rAF et que les rappels rAF sont courts (<15 ms).

Enfin, l'animation avec vsync ne s'applique pas uniquement aux animations d'interface utilisateur simples. Elle s'applique également aux animations Canvas2D, aux animations WebGL et même au défilement sur des pages statiques. Dans l'article suivant de cette série, nous allons examiner les performances de défilement en tenant compte de ces concepts.

Bonne animation !

Références