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 ne présentent aucun à-coup peut faire toute la différence ou maladroites et peu abouties.

Cet article est le premier d'une série sur l'optimisation des performances de rendu dans le navigateur. Pour commencer, nous allons voir pourquoi l'animation fluide est difficile et ce qui doit se passer pour y parvenir. Nous allons également vous donner quelques bonnes pratiques simples. Beaucoup de ces idées ont été initialement présentées dans « Jank Busters », que Nat Duca et moi avons donné lors de la conférence Google I/O (vidéo) cette année.

Présentation de V-sync

Les gamers sur PC connaissent ce terme, mais il n'est pas courant sur le Web. Qu'est-ce que 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 (V-sync) est une pratique qui consiste à générer de nouvelles images uniquement entre les actualisations d'é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 voulons que le contenu du frame mis en mémoire tampon change entre ces actualisations, et non pendant celles-ci ; Sinon, l'écran affiche la moitié d'une image et l'autre moitié, ce qui entraîne un "tearing".

Pour obtenir une animation fluide, vous devez disposer d'une nouvelle image à chaque actualisation de l'écran. Cela a deux implications majeures: le temps de rendu (c'est-à-dire le moment où le frame doit être prêt) et le budget des images (c'est-à-dire le temps dont le navigateur dispose pour produire un frame). Vous n'avez que le temps entre deux actualisations de l'é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 crucial: requestAnimationFrame

De nombreux développeurs Web utilisent setInterval ou setTimeout toutes les 16 millisecondes pour créer des animations. Il s'agit d'un problème pour plusieurs raisons (nous en reparlerons dans une minute), mais il est particulièrement préoccupant:

  • La résolution du minuteur à partir de JavaScript est de l'ordre de quelques millisecondes seulement
  • La fréquence d'actualisation varie en fonction de l'appareil

Rappelez-vous le problème de temps de rendu mentionné ci-dessus: pour être prêt avant l'actualisation suivante de l'écran, vous avez besoin d'une image d'animation terminée avec n'importe quel code JavaScript, manipulation DOM, mise en page, peinture, etc. Une faible résolution de minuteur peut rendre difficile l'exécution des images d'animation avant l'actualisation suivante de l'écran, mais la variation de la fréquence d'actualisation de l'écran rend cela impossible avec un minuteur fixe. Quel que soit l'intervalle du minuteur, vous vous éloignez lentement de la fenêtre de temps pour une image et finissez par en supprimer une. Cela se produisait même si le minuteur se déclenchait 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, et peut être affectée par des onglets en arrière-plan qui monétisent des ressources, etc. Même si cela est rare (par exemple, tous les 16 images parce que vous avez été retardé d'une milliseconde), vous remarquerez que vous perdrez plusieurs images par seconde. Vous ferez également le travail pour 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 votre application.

Les fréquences d'actualisation varient d'un écran à l'autre: la fréquence d'actualisation est de 60 Hz, mais celle de certains téléphones est de 59 Hz, la fréquence d'actualisation de certains ordinateurs portables est de 50 Hz en mode économie d'énergie, et la fréquence d'actualisation de certains écrans d'ordinateur de bureau est de 70 Hz.

Nous avons tendance à nous concentrer sur le nombre d'images par seconde (FPS) lorsque nous parlons de performances de rendu, mais la variance peut être un problème encore plus important. Nos yeux remarquent les petits accrocs irréguliers dans l'animation, qu'une animation au mauvais moment peut produire.

Pour obtenir des images d'animation correctement minutées, utilisez requestAnimationFrame. Lorsque vous utilisez cette API, vous demandez au navigateur une image 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 possède é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 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 produire le rappel moins fréquemment (par exemple, 30 fois par seconde sur un écran 60 Hz). Bien que la fréquence d'images diminue de moitié, l'animation reste cohérente. Et comme indiqué ci-dessus, nos yeux sont bien plus attentifs à la variance qu'à la fréquence d'images. Une fréquence de 30 Hz stable offre un meilleur rendu qu'une fréquence de 60 Hz qui manque quelques images par seconde.

requestAnimationFrame fait déjà l'objet de discussions. Pour en savoir plus, consultez des articles comme celui de Creative JS, 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 faire tout le travail pour créer 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 toute autre action que le navigateur doit effectuer pour extraire le cadre. Cela signifie que si le JavaScript dans votre rappel requestAnimationFrame prend plus de 16 ms à s'exécuter, vous n'avez aucun espoir de produire un frame à temps pour v-sync.

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

Le fait d'ouvrir la timeline des outils de développement et d'enregistrer rapidement cette animation en action montre que nous dépassons largement le budget lors de l'animation. Dans vos trajets, sélectionnez "Images". et jetez un coup d’œil:

<ph type="x-smartling-placeholder">
</ph> Une démonstration comportant beaucoup trop de mises en page
Une démonstration comportant beaucoup trop de mises en page
.

Ces rappels requestAnimationFrame (rAF) prennent plus de 200 ms. C'est un ordre de grandeur trop long pour dépasser 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, il y a beaucoup de mise en page.

La vidéo de Paul explique plus en détail la cause spécifique de la remise en page (il s'agit de scrollTop) et explique comment l'éviter. Mais le fait ici est que vous pouvez plonger dans le rappel et enquêter sur ce qui prend tant de temps.

<ph type="x-smartling-placeholder">
</ph> 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 le temps de rendu de 16 ms. Cet espace vide dans les cadres représente la marge de manœuvre dont vous disposez pour effectuer plus de travail (ou laisser le navigateur effectuer le travail nécessaire en arrière-plan). Cet espace vide est une bonne chose.

Autre source d'à-coups

La cause principale du problème lors de l'exécution d'animations JavaScript est que d'autres éléments peuvent entraver le rappel de la fonction rAF, et même l'empêcher de fonctionner. Même si votre rappel rAF est allégé et ne s'exécute que de 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 de mises à jour planifiées sur un minuteur) peuvent arrivent soudainement et s'exécutent sur n'importe quelle période sans céder. Sur mobile le traitement de ces événements peut prendre des centaines de millisecondes, Votre animation sera alors complètement bloquée. C'est ce que nous appelons d'accroches d'animation à-coups.

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

  • Évitez d'effectuer trop de traitements dans les gestionnaires d'entrée. lorsque vous faites beaucoup de code JS ou que vous essayez de réorganiser toute la page un gestionnaire onscroll est une cause très fréquente de très mauvaises à-coups.
  • Appliquez un maximum de traitements (lecture: tout ce dont l'exécution prend beaucoup de temps) dans votre rappel rAF ou vos web Workers.
  • Si vous transmettez la tâche dans le rappel rAF, essayez de la fragmenter afin de ne traiter qu'un peu chaque image ou de la retarder jusqu'à la fin d'une animation importante. Vous pourrez ainsi continuer à exécuter de courts rappels rAF et à l'animer en douceur.

Pour découvrir un excellent tutoriel expliquant comment transmettre le traitement dans des rappels requestAnimationFrame plutôt que dans des gestionnaires d'entrée, consultez l'article de Paul Lewis Leaner, Meaner, Accelerate Animations with requestAnimationFrame.

Animation CSS

Quoi de mieux que du code JavaScript léger dans vos rappels d'événement et de rAF ? Aucun code JS

Précédemment, nous avons dit qu'il n'existe pas de solution miracle pour éviter d'interrompre vos rappels rAF, mais vous pouvez utiliser des animations CSS pour éviter complètement d'en avoir besoin. Dans Chrome pour Android en particulier (et d'autres navigateurs travaillent sur des fonctionnalités similaires), les animations CSS présentent une propriété 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 instruction implicite concernant les à-coups: les navigateurs ne peuvent faire qu'une seule action à la fois. Ce n'est pas strictement vrai, mais il s'agit d'une bonne hypothèse de fonctionnement: à tout moment, le navigateur peut exécuter JavaScript, effectuer une mise en page ou peindre, mais une seule fois à la fois. Vous pouvez le vérifier dans la vue chronologique des outils de développement. Les animations CSS figurent parmi les exceptions à cette règle 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 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 des à-coups. En revanche, si nous exécutons cette animation avec des animations CSS, les à-coups ne se produisent plus.

(Au moment de la rédaction de ce document, les animations CSS ne comportent aucun à-coups 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 tels que celui-ci sur MDN.

Synthèse

En bref:

  1. Lors de l'animation, il est important de produire des images à chaque actualisation de l'écran. L'animation Vsync a un impact positif considérable sur la façon dont une application se sent.
  2. Le meilleur moyen d'obtenir des animations vsync dans Chrome et d'autres navigateurs récents est de pour utiliser l'animation CSS. Lorsque vous avez besoin de plus de flexibilité qu'avec les animations CSS la meilleure technique est l'animation basée sur requestAnimationFrame.
  3. Pour que les animations rAF restent opérationnelles et satisfaisantes, assurez-vous que les autres gestionnaires d'événements n'empêchent pas l'exécution de votre rappel rAF, et conservez les rappels rAF ; court (< 15 ms).

Enfin, les animations vsyncd ne s'appliquent pas qu'aux animations d'interface utilisateur simples, mais aussi aux animations Canvas2D et WebGL, et même au défilement sur des pages statiques. Dans l'article suivant de cette série, nous nous pencherons sur les performances de défilement en tenant compte de ces concepts.

Bonne animation !

Références