Améliorer les performances du canevas HTML5

Introduction

Le canevas HTML5, qui a été testé par Apple, est la norme la plus largement acceptée pour les éléments graphiques en mode immédiat 2D sur le Web. De nombreux développeurs s'en servent désormais pour une grande variété de projets multimédias, de visualisations et de jeux. Toutefois, à mesure que les applications que nous créons gagnent en complexité, les développeurs atteignent par inadvertance le mur de performances. Il existe de nombreuses idées reçues concernant l'optimisation des performances des canevas. Cet article vise à consolider une partie de ce corps en une ressource plus facile à assimiler pour les développeurs. Cet article présente des optimisations fondamentales qui s'appliquent à tous les environnements de graphisme informatique, ainsi que des techniques spécifiques aux canevas susceptibles d'être modifiées à mesure que les implémentations de canevas s'améliorent. En particulier, lorsque les fournisseurs de navigateurs implémentent l'accélération du GPU du canevas, certaines des techniques de performances décrites ici auront probablement moins d'impact. Cette information sera indiquée le cas échéant. Notez que cet article ne traite pas de l'utilisation du canevas HTML5. Pour en savoir plus, consultez ces articles relatifs au canevas sur HTML5Rocks, le chapitre du site Dive into HTML5 ou le tutoriel MDN Canvas.

Tests de performances

Pour faire face à l'évolution rapide du canevas HTML5, les tests de JSPerf (jsperf.com) vérifient que toutes les optimisations proposées fonctionnent toujours. YAML est une application Web qui permet aux développeurs d'écrire des tests de performances JavaScript. Chaque test se concentre sur un résultat que vous essayez d'obtenir (par exemple, effacer le canevas) et inclut plusieurs approches permettant d'obtenir le même résultat. Jetpack erf exécute chaque approche autant de fois que possible sur une courte période et attribue un nombre statistiquement pertinent d'itérations par seconde. Un score élevé, c'est toujours mieux ! Les visiteurs d'une page de test des performances de la page JSPerf peuvent exécuter le test dans leur navigateur et autoriser JSPerf à stocker les résultats normalisés dans Browserscope (browserscope.org). Étant donné que les techniques d'optimisation décrites dans cet article sont sauvegardées par un résultat JSPerf, vous pouvez revenir à des informations à jour pour savoir si la technique est toujours applicable ou non. J'ai écrit une petite application d'assistance qui affiche ces résultats sous forme de graphiques, intégrés tout au long de cet article.

Tous les résultats de performances présentés dans cet article sont associés à la version du navigateur. Il s'agit là d'une limite, car nous ne savons pas sur quel système d'exploitation le navigateur s'exécutait ni, plus important encore, si le canevas HTML5 a été accéléré par le matériel lors du test des performances. Pour savoir si le canevas HTML5 de Chrome est accéléré par le matériel, consultez about:gpu dans la barre d'adresse.

Précharger dans un canevas hors écran

Si vous redessinez des primitives similaires à l'écran sur plusieurs images, comme c'est souvent le cas lors de l'écriture d'un jeu, vous pouvez améliorer considérablement les performances en préchargeant de grandes parties de la scène. Le prérendu consiste à utiliser un canevas hors écran (ou canevas) distinct sur lequel afficher les images temporaires, puis à réafficher les canevas hors écran sur le canevas visible. Par exemple, supposons que vous redessiniez Mario en courant à 60 images par seconde. Vous pouvez soit redessiner son chapeau, sa moustache et sa lettre "M" sur chaque image, ou pré-afficher Mario avant d'exécuter l'animation. aucun prérendu:

// canvas, context are defined
function render() {
  drawMario(context);
  requestAnimationFrame(render);
}

prérendu:

var m_canvas = document.createElement('canvas');
m_canvas.width = 64;
m_canvas.height = 64;
var m_context = m_canvas.getContext('2d');
drawMario(m_context);

function render() {
  context.drawImage(m_canvas, 0, 0);
  requestAnimationFrame(render);
}

Notez l'utilisation de requestAnimationFrame, qui est abordée plus en détail dans une section ultérieure.

Cette technique est particulièrement efficace lorsque l'opération de rendu (drawMario dans l'exemple ci-dessus) est coûteuse. Un bon exemple de ceci est le rendu de texte, qui est une opération très coûteuse.

Cependant, les mauvaises performances du scénario de test "lâche" pré-rendu en sont les mauvaises. Lors du prérendu, il est important de vous assurer que votre canevas temporaire s'adapte parfaitement à l'image que vous dessinez, sinon l'amélioration des performances du rendu hors écran est contre-pondérée par la perte de performances due à la copie d'un grand canevas sur un autre (qui varie en fonction de la taille de la cible source). Dans le test ci-dessus, un canevas confortable est tout simplement plus petit:

can2.width = 100;
can2.height = 40;

Par rapport à la stratégie plus simple qui génère de mauvaises performances:

can3.width = 300;
can3.height = 100;

Regrouper des appels de canevas

Le dessin étant une opération coûteuse, il est plus efficace de charger la machine d'état de dessin avec un long ensemble de commandes, puis de toutes les vider dans le tampon vidéo.

Par exemple, lorsque vous tracez plusieurs lignes, il est plus efficace de créer un tracé contenant toutes les lignes et de le tracer avec un seul appel de traçage. En d'autres termes, plutôt que de tracer des lignes distinctes:

for (var i = 0; i < points.length - 1; i++) {
  var p1 = points[i];
  var p2 = points[i+1];
  context.beginPath();
  context.moveTo(p1.x, p1.y);
  context.lineTo(p2.x, p2.y);
  context.stroke();
}

Le tracé d'une seule polyligne est plus performant:

context.beginPath();
for (var i = 0; i < points.length - 1; i++) {
  var p1 = points[i];
  var p2 = points[i+1];
  context.moveTo(p1.x, p1.y);
  context.lineTo(p2.x, p2.y);
}
context.stroke();

Cela s'applique également à l'univers des canevas HTML5. Par exemple, lorsque vous tracez un tracé complexe, il est préférable d'y insérer tous les points plutôt que d'afficher les segments séparément (jsperf).

Notez toutefois qu'avec Canvas, il existe une exception importante à cette règle: si les primitives impliquées dans le dessin de l'objet souhaité ont de petits cadres de délimitation (par exemple, des lignes horizontales et verticales), il peut être plus efficace de les afficher séparément (jsperf).

Éviter les changements inutiles de l'état du canevas

L'élément de canevas HTML5 est implémenté sur une machine à états qui suit des éléments tels que les styles de remplissage et de trait, ainsi que les points précédents qui constituent le tracé actuel. Lorsque vous essayez d'optimiser les performances graphiques, il est tentant de vous concentrer uniquement sur le rendu graphique. Toutefois, la manipulation de la machine à états peut également entraîner une surcharge des performances. Si vous utilisez plusieurs couleurs de remplissage pour rendre une scène, par exemple, il est moins coûteux d'effectuer un rendu par couleur plutôt que par emplacement sur le canevas. Pour afficher un motif à rayures, vous pouvez afficher une bande, modifier les couleurs, afficher la bande suivante, etc. :

for (var i = 0; i < STRIPES; i++) {
  context.fillStyle = (i % 2 ? COLOR1 : COLOR2);
  context.fillRect(i * GAP, 0, GAP, 480);
}

Ou restituez toutes les bandes impaires, puis toutes les bandes paires:

context.fillStyle = COLOR1;
for (var i = 0; i < STRIPES/2; i++) {
  context.fillRect((i*2) * GAP, 0, GAP, 480);
}
context.fillStyle = COLOR2;
for (var i = 0; i < STRIPES/2; i++) {
  context.fillRect((i*2+1) * GAP, 0, GAP, 480);
}

Comme prévu, l'approche entrelacée est plus lente, car le changement de la machine d'état est coûteux.

Afficher uniquement les différences de l'écran, pas le nouvel état

Comme on peut s'y attendre, afficher moins d'annonces à l'écran est moins cher qu'afficher plus d'éléments. S'il n'y a que des différences incrémentielles entre les redessinages, vous pouvez améliorer considérablement les performances en dessinant simplement la différence. En d'autres termes, au lieu d'effacer tout l'écran avant de dessiner:

context.fillRect(0, 0, canvas.width, canvas.height);

Effectuez le suivi du cadre de délimitation tracé et ne l'effacez que.

context.fillRect(last.x, last.y, last.width, last.height);

Si vous êtes familiarisé avec l'infographie, vous pouvez également connaître cette technique sous le nom de "redessiner des régions", où le cadre de délimitation précédemment affiché est enregistré, puis effacé à chaque rendu. Cette technique s'applique également aux contextes de rendu basés sur les pixels, comme l'illustre cette présentation de l'émulateur Nintendo JavaScript.

Utiliser plusieurs canevas pour créer des scènes complexes

Comme indiqué précédemment, le dessin d'images volumineuses est coûteux et doit être évité dans la mesure du possible. En plus d'utiliser un autre canevas pour le rendu hors écran, comme illustré dans la section sur le prérendu, nous pouvons également utiliser des canevas en couches les uns sur les autres. Grâce à la transparence du canevas de premier plan, nous pouvons compter sur le GPU pour composer les valeurs alpha ensemble au moment de l'affichage. Vous pouvez configurer cela comme suit, avec deux canevas parfaitement positionnés l'un au-dessus de l'autre.

<canvas id="bg" width="640" height="480" style="position: absolute; z-index: 0">
</canvas>
<canvas id="fg" width="640" height="480" style="position: absolute; z-index: 1">
</canvas>

Par rapport à un seul canevas ici, l'avantage est que lorsque nous dessinons ou effaçons le canevas de premier plan, nous ne modifions jamais l'arrière-plan. Si votre jeu ou votre application multimédia peuvent être divisés en premier plan et en arrière-plan, envisagez de les afficher sur des canevas distincts pour améliorer considérablement les performances.

Vous pouvez souvent profiter de la perception humaine imparfaite pour afficher l'arrière-plan une seule fois ou à une vitesse inférieure à celle du premier plan (qui est susceptible d'occuper la plus grande partie de l'attention de l'utilisateur). Par exemple, vous pouvez effectuer le rendu du premier plan chaque fois que vous effectuez le rendu, mais n'effectuer le rendu en arrière-plan que tous les nièmes images. Notez également que cette approche se prête bien à la généralisation pour n'importe quel nombre de canevas composites si votre application fonctionne mieux avec ce type de structure.

Éviter le shadowBlur

Comme de nombreux autres environnements graphiques, le canevas HTML5 permet aux développeurs de flouter les primitives, mais cette opération peut s'avérer très coûteuse:

context.shadowOffsetX = 5;
context.shadowOffsetY = 5;
context.shadowBlur = 4;
context.shadowColor = 'rgba(255, 0, 0, 0.5)';
context.fillRect(20, 20, 150, 100);

Savoir différentes façons de vider le canevas

Comme le canevas HTML5 est un paradigme de dessin en mode immédiat, la scène doit être redessinée explicitement à chaque image. De ce fait, le nettoyage du canevas est une opération fondamentale pour les applications et les jeux Canvas HTML5. Comme indiqué dans la section Éviter les changements d'état du canevas, il n'est souvent pas souhaitable d'effacer tout le canevas. Toutefois, si vous devez le faire, deux options s'offrent à vous: appeler context.clearRect(0, 0, width, height) ou utiliser un piratage spécifique au canevas : canvas.width = canvas.width;. Au moment de la rédaction, clearRect offre généralement de meilleures performances que la version réinitialisée en largeur, mais dans certains cas, le piratage de la réinitialisation canvas.width est beaucoup plus rapide dans Chrome 14.

Soyez prudent avec ce conseil, car il dépend en grande partie de l'implémentation du canevas sous-jacent et est très susceptible de changer. Pour en savoir plus, consultez l'article de Simon Sarris sur l'effacement du canevas.

Éviter les coordonnées à virgule flottante

Le canevas HTML5 est compatible avec le rendu des sous-pixels, et il n'est pas possible de le désactiver. Si vous dessinez avec des coordonnées qui ne sont pas des entiers, il utilise automatiquement l'anticrénelage pour essayer de lisser les lignes. Voici l'effet visuel, extrait de cet article sur les performances du canevas de sous-pixel par Seb Lee-Delisle:

Sous-pixel

Si le lutin lissé n'est pas l'effet que vous recherchez, il peut être beaucoup plus rapide de convertir vos coordonnées en entiers à l'aide de Math.floor ou Math.round (jsperf):

Pour convertir vos coordonnées à virgule flottante en entiers, vous pouvez utiliser plusieurs techniques intelligentes. La plus performante consiste à ajouter une moitié au nombre cible, puis à effectuer des opérations bit à bit sur le résultat afin d'éliminer la partie fractionnaire.

// With a bitwise or.
rounded = (0.5 + somenum) | 0;
// A double bitwise not.
rounded = ~~ (0.5 + somenum);
// Finally, a left bitwise shift.
rounded = (0.5 + somenum) << 0;

La répartition complète des performances est disponible ici (jsperf).

Notez que ce type d'optimisation ne devrait plus avoir d'importance une fois que les implémentations de canevas seront accélérées par le GPU, ce qui permettra d'afficher rapidement des coordonnées non entières.

Optimiser vos animations avec requestAnimationFrame

L'API requestAnimationFrame, qui est relativement récente, est la méthode recommandée pour implémenter des applications interactives dans le navigateur. Plutôt que de demander au navigateur d'effectuer l'affichage à un taux de tic-tac fixe particulier, vous lui demandez poliment d'appeler votre routine de rendu et d'être appelé lorsque le navigateur sera disponible. Par ailleurs, si la page n'est pas au premier plan, le navigateur est assez intelligent pour ne pas s'afficher. Le rappel requestAnimationFrame vise un taux de rappel de 60 FPS, mais ne le garantit pas. Vous devez donc savoir combien de temps s'est écoulé depuis le dernier rendu. Cela peut se présenter comme suit:

var x = 100;
var y = 100;
var lastRender = Date.now();
function render() {
  var delta = Date.now() - lastRender;
  x += delta;
  y += delta;
  context.fillRect(x, y, W, H);
  requestAnimationFrame(render);
}
render();

Notez que cette utilisation de requestAnimationFrame s'applique au canevas ainsi qu'à d'autres technologies de rendu telles que WebGL. Au moment de la rédaction de ce document, cette API n'est disponible que dans Chrome, Safari et Firefox. Vous devez donc utiliser ce shim.

La plupart des implémentations de canevas pour mobile sont lentes

Parlons des mobiles. Malheureusement au moment de la rédaction de ce document, seule la version bêta d'iOS 5.0 exécutant Safari 5.1 propose une implémentation du canevas pour mobile avec accélération par GPU. Sans accélération du GPU, les navigateurs mobiles ne disposent généralement pas de processeurs assez puissants pour les applications modernes basées sur les canevas. Un certain nombre des tests JSPerf décrits ci-dessus fonctionnent beaucoup moins bien sur mobile que sur ordinateur, ce qui limite considérablement les types d'applications multi-appareils auxquelles vous pouvez vous attendre à s'exécuter correctement.

Conclusion

En résumé, cet article présente un ensemble complet de techniques d'optimisation utiles qui vous aideront à développer des projets performants basés sur des canevas HTML5. Maintenant que vous en savez plus, optimisez vos créations. Si vous n'avez pas encore de jeu ou d'application à optimiser, découvrez les tests Chrome et Creative JS pour trouver des idées.

Références