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

Tom Wiltzius
Tom Wiltzius

Introduction

Les animations, les transitions et les autres petits effets d'interface utilisateur doivent être fluides et réactifs. S'assurer que ces effets sont exempts d'à-coups peut faire la différence entre une sensation « natif » ou une sensation maladroite et peu soignée.

Cet article est le premier d'une série d'articles portant sur l'optimisation des performances de rendu dans le navigateur. Pour commencer, nous verrons pourquoi la fluidité de l'animation est difficile et ce qu'il faut faire 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 avons donnée lors de la conférence Google I/O (vidéo) cette année.

Présentation de V-sync

Ce terme est peut-être familier aux gamers sur PC, mais il est peu courant sur le Web: qu'est-ce que v-sync ?

Pensez à 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 V (ou synchronisation verticale) fait référence à la pratique consistant à générer de nouveaux frames uniquement entre deux actualisations de l'écran. Vous pouvez considérer cela comme une condition de concurrence entre le processus qui écrit des données dans le tampon de l'écran et le système d'exploitation qui lit ces données pour les afficher à l'écran. Nous souhaitons que le contenu des frames en mémoire tampon change entre ces actualisations, et non pendant ces actualisations. Dans le cas contraire, l'écran affiche la moitié d'une image et la moitié d'une autre, ce qui génère un "tear-tearing".

Pour obtenir une animation fluide, une nouvelle image doit être prête à chaque actualisation de l'écran. Cela a deux grandes implications: le temps de rendu (c'est-à-dire, le moment où le frame doit être prêt) et le budget de frame (c'est-à-dire, le temps que le navigateur doit produire pour produire un frame). Vous n'avez que le temps entre les actualisations d'écran pour terminer un frame (environ 16 ms sur un écran 60 Hz) et vous souhaitez commencer à produire l'image suivante dès que la dernière s'affiche à 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. Ce problème peut survenir pour diverses raisons (nous en reparlerons dans une minute), mais les points suivants sont particulièrement préoccupants:

  • La résolution du minuteur de JavaScript ne prend que quelques millisecondes
  • Les fréquences d'actualisation varient selon les appareils.

Rappelez-vous le problème de temps de rendu mentionné ci-dessus: vous avez besoin d'un frame d'animation terminé, avec tout code JavaScript, manipulation DOM, mise en page, peinture, etc., pour être prêt avant l'actualisation suivante de l'écran. Une faible résolution du minuteur peut rendre difficile l'achèvement des frames d'animation avant la prochaine actualisation de l'écran, mais les variations de la fréquence d'actualisation de l'écran le rendent impossible avec un minuteur fixe. Quel que soit l'intervalle du minuteur, vous quitterez lentement la fenêtre de temps d'une image et finirez par en abandonner une. Cela se produit même si le minuteur s'est déclenché avec une précision de l'ordre de la milliseconde, ce qui n'est pas le cas (comme l'ont découvert les développeurs). La résolution du minuteur varie selon que la machine est sur batterie ou branchée, peut être affectée par des 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 êtes éteint d'une milliseconde), vous remarquerez que vous abandonnez plusieurs images par seconde. Vous allez également générer des frames qui ne s'affichent jamais, ce qui gaspille de l'énergie et du temps CPU que vous pourriez consacrer à d'autres tâches dans l'application.

La fréquence d'actualisation diffère selon les écrans: la fréquence d'actualisation de 60 Hz est courante, mais certains téléphones ont une fréquence de 59 Hz, certains ordinateurs portables chutent à 50 Hz en mode basse consommation et certains écrans d'ordinateur de bureau ont une fréquence de 70 Hz.

Lorsque nous examinons les performances d'affichage, nous avons tendance à nous concentrer sur le nombre d'images par seconde (FPS), mais la variance peut être un problème encore plus important. Nos yeux remarquent les minuscules problèmes d'animation que peut produire une animation au mauvais moment.

requestAnimationFrame vous permet d'obtenir des images d'animation correctement planifiées. Lorsque vous utilisez cette API, vous demandez au navigateur un frame d'animation. Votre rappel est appelé lorsque le navigateur va bientôt produire un nouveau frame. quelle que soit la fréquence d'actualisation.

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

  • Les animations des onglets en arrière-plan sont mises en pause, ce qui préserve les ressources système et l'autonomie de la batterie.
  • Si le système ne peut pas gérer l'affichage avec 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 la fréquence d'images soit divisée par deux, cela permet de maintenir la cohérence de l'animation. Comme indiqué ci-dessus, nos yeux sont beaucoup plus attentifs à la variance que la fréquence d'images. Une fréquence de 30 Hz est meilleure qu'une fréquence de 60 Hz qui manque quelques images par seconde.

requestAnimationFrame fait déjà l'objet d'une discussion générale sur le sujet. Pour en savoir plus, consultez des articles comme celui-ci de JavaScript sur les créations, mais il s'agit d'une première étape importante pour fluidifier l'animation.

Budget frame

Étant donné que nous voulons qu'un nouveau frame soit prêt à chaque actualisation de l'écran, il n'y a que le temps entre les actualisations pour effectuer le travail nécessaire à la 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, l'affichage et toute autre opération requise par le navigateur pour extraire le frame. Cela signifie que si l'exécution du code JavaScript dans votre rappel requestAnimationFrame prend plus de 16 ms, vous n'avez aucun espoir de produire un frame à temps pour v-sync.

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

En ouvrant la timeline des outils de développement et en enregistrant cette animation en action, vous pouvez rapidement voir que nous avons largement dépassé le budget lors de l'animation. Dans la timeline, passez à "Images" et examinez les éléments suivants:

Une démo avec beaucoup trop de mise en page
Démonstration 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, la mise en page est importante.

La vidéo de Paul explique plus en détail la cause spécifique de la remise en page (voir scrollTop) et comment l'éviter. Mais le point ici est que vous pouvez vous plonger dans le rappel et déterminer ce qui prend si longtemps.

Une démo mise à jour avec une mise en page beaucoup plus réduite
Une démo mise à jour avec une mise en page nettement réduite

Notez les temps de rendu de 16 ms. Cet espace vide dans les cadres est la marge de progression dont vous disposez pour effectuer plus de travail (ou laisser le navigateur faire ce qu'il doit faire en arrière-plan). Cet espace vide est une bonne chose.

Autre source d'à-coups

La principale cause de problème lors de l'exécution d'animations JavaScript est que d'autres éléments peuvent empêcher votre rappel rAF, voire empêcher son exécution. Même si votre rappel rAF est léger et s'exécute en quelques millisecondes, d'autres activités (comme le traitement d'une requête 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 arriver et s'exécuter pendant n'importe quelle période sans délai. Sur les appareils mobiles, le traitement de ces événements peut parfois prendre des centaines de millisecondes, au cours desquelles votre animation est complètement bloquée. Nous appelons ces accroches d'animation des à-coups.

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

  • Limitez le traitement dans les gestionnaires d'entrée. Le fait d'utiliser beaucoup de code JS ou d'essayer de réorganiser la page entière pendant que vous utilisez un gestionnaire onscroll, par exemple, est une cause très courante d'à-coups.
  • Appliquez autant de traitements que possible (à savoir tout ce dont l'exécution prendra beaucoup de temps) dans votre rappel rAF ou dans Web Workers.
  • Si vous transférez la tâche dans le rappel rAF, essayez de la fragmenter afin d'effectuer un traitement limité de chaque image ou de la retarder jusqu'à la fin d'une animation importante. De cette façon, vous pouvez continuer à exécuter de courts rappels rAF et à effectuer une animation fluide.

Pour découvrir un tutoriel efficace qui explique comment déployer le traitement dans des rappels requestAnimationFrame plutôt que des gestionnaires d'entrée, consultez l'article de Paul Lewis intitulé Leaner, Meaner, Faster Animations with requestAnimationFrame.

Animation CSS

Quoi de mieux que le langage JS léger dans vos rappels d'événements et rAF ? Aucun JS.

Nous avons vu précédemment qu'il n'existe pas de solution miracle pour éviter d'interrompre vos rappels rAF, mais vous pouvez utiliser l'animation CSS pour ne pas en avoir complètement besoin. Sur Chrome pour Android en particulier (et d'autres navigateurs travaillent sur des fonctionnalités similaires), la propriété des animations CSS est très souhaitable : le navigateur peut souvent les exécuter, même si JavaScript est en cours d'exécution.

La section ci-dessus contient une déclaration implicite concernant les à-coups: les navigateurs ne peuvent effectuer qu'une action à la fois. Ce n'est pas absolument vrai, mais il est judicieux de l'avoir: à tout moment, le navigateur peut exécuter JavaScript, effectuer une mise en page ou une peinture, mais seulement un par un. Vous pouvez vérifier cela dans la vue chronologique des outils de développement. L'une des exceptions à cette règle concerne les animations CSS dans Chrome pour Android (et bientôt dans Chrome pour ordinateur, mais pas encore).

Lorsque cela est possible, l'utilisation d'une animation CSS simplifie votre application et permet l'exécution fluide des animations, même lorsque JavaScript est exécuté.

  // 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 entraîne des à-coups. En revanche, si nous générons cette animation à l'aide d'animations CSS, les à-coups ne se produisent plus.

N'oubliez pas qu'au moment de la rédaction de ce document, l'animation CSS n'est sans à-coups 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.

Synthèse

Voici un extrait:

  1. Lors des animations, il est important de produire des images à chaque actualisation de l'écran. L'animation vsync a un impact positif considérable sur l'apparence d'une application.
  2. Le meilleur moyen d'obtenir une animation vsync'd dans Chrome et dans d'autres navigateurs récents consiste à utiliser une animation CSS. Lorsque vous avez besoin de plus de flexibilité que l'animation CSS, la meilleure technique consiste à utiliser une animation basée sur requestAnimationFrame.
  3. Pour que les animations rAF restent opérationnelles et satisfaisantes, assurez-vous que d'autres gestionnaires d'événements ne bloquent pas l'exécution de votre rappel rAF, et veillez à ce que les rappels rAF soient courts (<15 ms).

Enfin, l'animation vsync'd ne s'applique pas qu'aux animations d'interface utilisateur simples, mais également à l'animation Canvas2D, à l'animation WebGL et même au défilement sur des pages statiques. Dans le prochain article de cette série, nous aborderons les performances du défilement en tenant compte de ces concepts.

Bonne animation !

Références