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. Il existe de nombreuses idées reçues sur l'optimisation des performances du canevas. 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, à mesure que les fournisseurs de navigateurs implémenteront l'accélération GPU du canevas, certaines des techniques de performances décrites deviendront probablement moins efficaces. 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 s'adapter à l'évolution rapide du canevas HTML5, les 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 performances 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'assistance qui affiche ces résultats sous forme de graphiques, intégrés dans cet article.
Tous les résultats de performances de cet article sont basés sur 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 images, 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 en courant à 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 la vider dans le tampon vidéo.
Par exemple, lorsque vous dessinez plusieurs lignes, il est plus efficace de créer un seul 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 améliorées 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 au 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 afficher une scène, par exemple, il est moins coûteux d'effectuer le rendu par couleur que par emplacement sur le canevas. Pour afficher un motif à fines rayures, vous pouvez afficher une rayure, changer de couleur, afficher la prochaine rayure, 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);
Gardez une trace du cadre de délimitation dessiné et ne le supprimez que.
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 l'illustre cette conférence sur l'émulateur Nintendo en JavaScript.
Utiliser plusieurs canevas superposés pour des scènes complexes
Comme indiqué précédemment, le dessin de grandes images est coûteux et doit être évité si possible. En plus d'utiliser un autre canevas pour le rendu hors écran, comme illustré dans la section "Pré-rendu", vous pouvez également utiliser des canevas superposés. 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 au 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 des primitives, mais cette opération peut être 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. Par conséquent, effacer le canevas est une opération fondamentalement importante pour les applications et les jeux HTML5 sur canevas.
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.
Faites attention à ce conseil, car il dépend fortement de l'implémentation du canevas sous-jacent et est très sujet à modification. 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 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 au niveau du sous-pixel de Seb Lee-Delisle:

Si l'effet de sprite lisse n'est pas celui 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 des appareils mobiles. 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 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
- Mode immédiat par rapport au mode conservé.
- Autres articles sur HTML5 Canvas de HTML5Rocks
- Section Canvas de l'article "Découvrir HTML5".
- JSPerf permet aux développeurs de créer des tests de performances JavaScript.
- Browserscope stocke les données de performances du navigateur.
- JSPerfView, qui affiche les tests JSPerf sous forme de graphiques.
- Article de blog de Simon sur l'effacement du canevas et son livre HTML5 Unleashed, qui inclut des chapitres sur les performances du canevas.
- Article de blog de Sebastian sur les performances de rendu au niveau des sous-pixels
- Présentation de Ben sur l'optimisation d'un émulateur NES JS
- Nouveau profileur de canevas dans les outils pour les développeurs Chrome.