Aujourd'hui, je vous propose de jeter un oeil à l'optimisation canvas en se basant sur l'optimisation d'un cellular automata.

Commençons avec le contexte

Tout commence-il y a quelques semaines, avec mon envie de faire un cellular automata pour un article. Trouvant le DOM plus simple à comprendre et a vulgarisé que Canvas, j'ai décidé d'utiliser Vue et <table> pour mon implementation.

See the Pen John Conway's Game of life using Vue.js – Maze version by Benjamin SANCHEZ (@B_Sanchez) on CodePen.

L'exercice en soi n'est pas très compliqué, mais comme on pouvait s'y attendre les performances ne sont pas vraiment a la hauteur, ce qui m'a poussé a faire de l'optimisation.

Les differents moyens d'imprimer un pixel sur un canvas

HTML5 Canvas n'existe pas pour rien, et ce genre d'exercice est parfaitement dans sa fiche de poste, il n'y a pas de doute sur la pertinence de son utilisation, mais comment  "dessiner" notre pixel sur le canvas de la maniere la plus efficace possible ?

Gagnez du temps dans vos créations graphiques !

+ 1,6 millions de ressources (photos, mockups, themes Wordpress etc.) premium à votre disposition pour livrer plus rapidement.

Nous avons deux solution a notre porté. Pour commencer, la plus evidente est fillRect(x, y, 1, 1) qui permet tout simplement de dessiner un rectangle de 1px sur 1px.

Plus fourbe, il serait possible de creer un "tampon" avec context.createImageData(1, 1), de manipuler l'objet data de ce tampon pour lui donner une couleur (le "tremper dans l'encre"), puis de l'appliquer ou on le souhaite sur le canvas.

// A executer qu'une seul fois, la creation du tampon
let tampon = context.createImageData(1, 1)
var data = tampon.data;

// La definition de la couleur, en RGB
data[0] = red;
data[1] = green;
data[2] = blue;
data[3] = alpha;
// Enfin, on tamponne
context.putImageData(tampon, x, y);

Comment déterminer la meilleure solution parmi les deux ? Simple, un benchmark !

Comme vous pouvez le voir, la solution 2 (qui est la 3 sur jsperf) est tres clairement la plus interessante des deux, de beaucoup.

Notez qu'il existe une 3eme solution, avec des performances pitoyable : Récupérer l'image Data de l'image complète, dessiner un seul pixel, puis remplacer l'image complète. Ne vous laissez pas berner par les stats, si cette technique est nul pour un seul pixel, elle est bien plus intéressante s'il y a plus d'un pixel qui a changé.

Traitement 8bits vs traitement 32bits

Bon, passer au canvas a déjà fait une grosse différence au niveau des perfs, mais y a-t-il moyen d'aller plus vite ? Oui, avec les opérateurs bit à bit !

L'idée ici est donc de manipuler le tableau à travers une interface 32 bits au lieu d'une interface 8 bits. Dans cette interface, notre couleur RGBA est considéré comme un seul objet UInt, qu'il est possible de manipuler grace aux opérateurs bit à bit de décalage.

buf32[index] =
  (255 << 24) |	// alpha
  (c.b << 16) |	// blue
  (c.g <<  8) |	// green
   c.r          // red
}

Le résultat est un petit gain de vitesse supplémentaire non négligeable.

En définitive et après quelques optimisations supplémentaires (limiter les boucles au maximum, stocker les données directement sous forme de buffer, décrémenter plutôt qu'incrémenter, …) nous arrivons à un résultat totalement acceptable !

Pour aller plus loin, je vous propose de creuser directement dans mon code, que vous pourrez retrouver à cette adresse.

Je pense qu'il y aurait encore d'autres moyens d'optimiser le résultat, si vous avez des idées à me proposer je vous attends en commentaire !