Améliorer les performances du canevas HTML5

Introduction

Le canevas HTML5, qui a commencé comme un test d'Apple, est la norme la plus largement acceptée pour les graphiques en mode immédiat 2D sur le Web. De nombreux développeurs s'appuient désormais sur elle pour une grande variété de projets multimédias, de visualisations et de jeux. Cependant, à mesure que la complexité des applications que nous créons augmente, les développeurs atteignent involontairement le mur des performances. L'optimisation des performances du canevas n'est pas une mince affaire. Cet article vise à regrouper une partie de cette documentation dans une ressource plus facile à assimiler pour les développeurs. Cet article inclut des optimisations fondamentales qui s'appliquent à tous les environnements de graphisme informatique, ainsi que des techniques spécifiques au canevas qui sont susceptibles d'évoluer à mesure que les implémentations de canevas s'améliorent. En particulier, lorsque les fournisseurs de navigateurs implémentent l'accélération GPU du canevas, certaines des techniques de performances décrites qui ont été décrites auront probablement moins d'impact. Cela sera indiqué le cas échéant. Notez que cet article n'aborde pas l'utilisation du canevas HTML5. Pour ce faire, consultez ces articles sur le canevas sur HTML5Rocks, ce chapitre sur le site Dive into HTML5 ou le tutoriel sur le canevas MDN.

Tests de performances

Pour faire face à l'évolution rapide du canevas HTML5, des tests JSPerf (jsperf.com) vérifient que chaque optimisation proposée fonctionne toujours. JSPerf 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 qui permettent d'obtenir le même résultat. JSPerf exécute chaque approche autant de fois que possible sur une courte période et fournit un nombre d'itérations par seconde statistiquement pertinent. Plus le score est élevé, mieux c'est ! Les visiteurs d'une page de test de performance JSPerf peuvent exécuter le test dans leur navigateur et laisser JSPerf stocker les résultats normalisés du test sur Browserscope (browserscope.org). Étant donné que les techniques d'optimisation de cet article sont étayées par un résultat JSPerf, vous pouvez revenir pour obtenir des informations à jour sur l'application ou non de la technique. J'ai écrit une petite application d'aide 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 liés à la version du navigateur. Cela s'avère être une limitation, car nous ne savons pas sur quel OS le navigateur s'exécutait, ni, plus important encore, si le canevas HTML5 était accéléré matériellement lors du test de performances. Pour savoir si le canevas HTML5 de Chrome est accéléré matériellement, accédez à about:gpu dans la barre d'adresse.

Prérendu sur un canevas hors écran

Si vous redessinez des primitives similaires à l'écran sur plusieurs frames, comme c'est souvent le cas lorsque vous écrivez un jeu, vous pouvez obtenir de grands gains de performances en pré-rendant de grandes parties de la scène. Le pré-rendu consiste à utiliser un canevas (ou plusieurs canevas) distincts hors écran sur lesquels effectuer le rendu d'images temporaires, puis à effectuer le rendu des canevas hors écran sur celui visible. Par exemple, supposons que vous redessinez Mario avec 60 images par seconde. Vous pouvez redessiner son chapeau, sa moustache et le "M" à chaque frame, ou pré-rendre Mario avant d'exécuter l'animation. pas de 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 sera 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. Le rendu du texte, qui est une opération très coûteuse, en est un bon exemple.

Cependant, les performances médiocres du scénario de test "pré-rendu lâche". Lors du pré-rendu, il est important de s'assurer que votre canevas temporaire s'adapte parfaitement à l'image que vous dessinez. Sinon, le gain de performances du rendu hors écran est contrebalancé par la perte de performances liée à 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 bien ajusté est simplement plus petit:

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

Par rapport à la configuration lâche qui offre des performances moins bonnes:

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

Regrouper les appels de canevas

Étant donné que le dessin est 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 les vider toutes 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 dessiner avec un seul appel de dessin. En d'autres termes, au lieu 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();
}

Des performances meilleures en dessinant une seule polyligne:

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 aux canevas HTML5. Lorsque vous dessinez un tracé complexe, par exemple, il est préférable de placer tous les points dans le tracé plutôt que de les afficher 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 petites boîtes de délimitation (par exemple, des lignes horizontales et verticales), il peut être plus efficace de les afficher séparément (jsperf).

Éviter les modifications d'état du canevas inutiles

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 se concentrer uniquement sur le rendu graphique. Toutefois, la manipulation de la machine à états peut également entraîner une surcharge de performances. Si vous utilisez plusieurs couleurs de remplissage pour effectuer le rendu d'une scène, par exemple, il est moins coûteux de le rendre par couleur plutôt que de le placer sur le canevas. Pour afficher un motif de 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);
}

Vous pouvez également afficher 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 la modification de la machine à états est coûteuse.

Affichez uniquement les différences entre les écrans, et non l'ensemble du nouvel état.

Comme on peut s'y attendre, le rendu d'éléments moins nombreux à l'écran est moins coûteux que le rendu d'éléments plus nombreux. Si vous n'avez que des différences incrémentielles entre les redessins, vous pouvez améliorer considérablement les performances en dessinant simplement la différence. En d'autres termes, plutôt que d'effacer l'intégralité de l'écran avant de dessiner:

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

Effectuez le suivi du cadre de délimitation tracé et supprimez-le uniquement.

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

Si vous connaissez les graphiques IT, vous connaissez peut-être également cette technique sous le nom de "régions de redessin", où la zone de délimitation précédemment affichée est enregistrée, puis effacée à chaque affichage. Cette technique s'applique également aux contextes de rendu basés sur les pixels, comme illustré dans cette présentation de l'émulateur Nintendo JavaScript.

Utiliser plusieurs canevas superposés pour des scènes complexes

Comme indiqué précédemment, le dessin d'images volumineuses coûte cher et doit être évité autant que possible. En plus d'utiliser un autre canevas pour le rendu en dehors de l'écran, comme illustré dans la section sur le prérendu, nous pouvons également utiliser des toiles superposées les unes sur les autres. En utilisant la transparence dans le canevas de premier plan, nous pouvons nous appuyer sur le GPU pour composer les alphas au moment du rendu. Vous pouvez configurer cela comme suit, avec deux canevas positionnés de manière absolue l'un sur 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>

L'avantage de ne pas avoir qu'un seul canevas ici 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 peut être divisé 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 tirer parti de la perception humaine imparfaite et n'afficher l'arrière-plan qu'une seule fois ou à une vitesse plus lente que l'avant-plan (qui occupe probablement la majeure partie de l'attention de l'utilisateur). Par exemple, vous pouvez effectuer le rendu du premier plan à chaque fois que vous effectuez un rendu, mais n'effectuer le rendu de l'arrière-plan que tous les N frames. Notez également que cette approche se généralise bien pour un nombre quelconque de canevas composites si votre application fonctionne mieux avec ce type de structure.

Éviter 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);

Connaître différentes façons de vider le canevas

Étant donné que le canevas HTML5 est un paradigme de dessin en mode immédiat, la scène doit être redessinée explicitement à chaque frame. C'est pourquoi le nettoyage du canevas est une opération essentielle pour les applications et les jeux de canevas HTML5. Comme indiqué dans la section Éviter les modifications de l'état du canevas, il est souvent déconseillé d'effacer l'intégralité du canevas. Toutefois, si vous devez le faire, deux options s'offrent à vous: appeler context.clearRect(0, 0, width, height) ou utiliser un hack spécifique au canevas : canvas.width = canvas.width. Au moment de la rédaction de cet article, clearRect est généralement plus performant que la version de réinitialisation de la largeur, mais dans certains cas, l'utilisation du hack de réinitialisation canvas.width est beaucoup plus rapide dans Chrome 14.

Soyez prudent avec ce conseil, car il dépend fortement 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 la suppression du canevas.

Éviter les coordonnées à virgule flottante

Le canevas HTML5 est compatible avec le rendu au niveau du sous-pixel, et il n'est pas possible de le désactiver. Si vous dessinez avec des coordonnées qui ne sont pas des entiers, l'anticrénelage est automatiquement utilisé pour essayer d'adoucir les lignes. Voici l'effet visuel, tiré de cet article sur les performances du canevas en sous-pixels de 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 astucieuses, la plus performante consistant à ajouter une demi-unité au nombre cible, puis à effectuer des opérations au niveau du bit sur le résultat pour é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;

Vous trouverez ici la répartition complète des performances (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 GPU, ce qui permettra d'afficher rapidement des coordonnées non entières.

Optimiser vos animations avec requestAnimationFrame

L'API requestAnimationFrame 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 le rendu à un taux de rafraîchissement fixe particulier, vous demandez poliment au navigateur d'appeler votre routine de rendu et d'être appelé lorsque le navigateur est disponible. En guise d'effet secondaire intéressant, si la page n'est pas au premier plan, le navigateur est suffisamment intelligent pour ne pas l'afficher. Le rappel requestAnimationFrame vise un taux de rappel de 60 FPS, mais ne le garantit pas. Vous devez donc suivre le temps é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 cet article, cette API n'est disponible que dans Chrome, Safari et Firefox. Vous devez donc utiliser ce shim.

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

Parlons maintenant du mobile. Malheureusement, au moment de la rédaction de cet article, seule la version bêta d'iOS 5.0 exécutant Safari 5.1 propose une implémentation du canevas mobile accélérée par GPU. Sans accélération GPU, les navigateurs mobiles ne disposent généralement pas de processeurs suffisamment puissants pour les applications modernes basées sur le canevas. Un certain nombre des tests JSPerf décrits ci-dessus sont moins performants d'un ordre de grandeur sur mobile que sur ordinateur, ce qui limite considérablement les types d'applications multi-appareils que vous pouvez exécuter avec succès.

Conclusion

Pour résumer, cet article a présenté un ensemble complet de techniques d'optimisation utiles qui vous aideront à développer des projets performants basés sur le canevas HTML5. Maintenant que vous avez appris de nouvelles choses, allez-y et optimisez vos superbes créations. Si vous n'avez pas de jeu ou d'application à optimiser pour le moment, consultez Chrome Experiments et Creative JS pour trouver l'inspiration.

Références