49 votes

Performance et optimisation de HTML5 Canvas - Conseils, astuces et meilleures pratiques de codage

CONNAISSEZ-VOUS D'AUTRES BONNES PRATIQUES POUR LES CANEVAS ?

Veuillez ajouter à ce fil de discussion ce que vous savez, ce que vous avez appris ou ce que vous avez lu en ligne sur les meilleures pratiques, les trucs et astuces pour les performances de Canvas.

Canvas étant encore très récent sur Internet, et ne présentant aucun signe de vieillissement à ce que je vois dans le futur, il n'y a pas beaucoup de "meilleures pratiques" documentées ou d'autres conseils vraiment importants à connaître absolument pour développer avec Canvas à un endroit particulier. Les choses de ce genre sont éparpillées un peu partout et souvent sur des sites moins connus.

Il y a tellement de choses que les gens doivent savoir, et encore tellement de choses à apprendre aussi.


J'ai voulu partager quelques éléments pour aider les personnes qui apprennent à utiliser Canvas et peut-être même celles qui le connaissent déjà bien. J'espère obtenir des réactions de la part d'autres personnes sur ce qu'elles considèrent comme les meilleures pratiques ou d'autres trucs et astuces pour travailler avec Canvas en HTML5.

Je voudrais commencer par une chose que j'ai personnellement trouvée très utile, mais étonnamment peu fréquente chez les développeurs.

1. Mettez votre code en retrait

Comme vous le feriez à tout autre moment, dans toute autre langue, quel que soit le cas. C'est une bonne pratique pour tout le reste, et je me suis rendu compte que dans une application complexe en canevas, les choses peuvent devenir un peu confuses lorsqu'on a affaire à plusieurs contextes différents et à des états de sauvegarde/restauration. Sans oublier que le code est tout simplement plus lisible et plus propre.

Par exemple :

...
// Try to tell me this doesn't make sense to do
ctx.fillStyle = 'red';
ctx.fill();
ctx.save();
    if (thing < 3) {
        // indenting
        ctx.beginPath();
            ctx.arc(2, 6, 11, 0, Math.PI*2, true);
        ctx.closePath();
        ctx.beginPath();
            ctx.moveTo(20, 40);
            ctx.lineTo(10, 200);
            ctx.moveTo(20, 40);
            ctx.lineTo(100, 40);
        ctx.closePath();
        ctx.save();
            ctx.fillStyle = 'blue'
            ctx.fill();
        ctx.restore();
    } else { 
        // no indenting
        ctx.drawImage(img, 0, 0, 200, 200);
        ctx.save();
        ctx.shadowBlur();
        ctx.beginPath();
        ctx.arc(2, 60, 10, 0, Math.PI*2, false);
        ctx.closePath();
        ctx.fillStyle 'green';
        ctx.fill();
        ctx.restore();
    }
ctx.restore();
ctx.drawRect();
ctx.fill();
...

L'instruction IF n'est-elle pas plus facile et plus propre à lire et à savoir ce qui se passe immédiatement que l'instruction ELSE dans ce cas ? Vous voyez ce que je veux dire ? Je pense que c'est une méthode que les développeurs devraient continuer à pratiquer comme ils le feraient pour écrire du bon vieux javascript ou tout autre langage.

Utiliser requestAnimationFrame au lieu de setInterval / setTimeout

setInterval et setTimeout n'ont jamais été conçus pour être utilisés comme des minuteurs d'animation, ce sont juste des méthodes génériques pour appeler des fonctions après un délai. Si vous définissez un intervalle de 20 ms dans le futur, mais que votre file d'attente de fonctions prend plus de temps à s'exécuter, votre minuterie ne se déclenchera qu'après la fin de ces fonctions. Cela peut prendre un certain temps, ce qui n'est pas idéal en matière d'animation. RequestAnimationFrame est une méthode qui indique au navigateur qu'une animation est en cours, afin qu'il puisse optimiser les repeints en conséquence. Elle réduit également l'animation pour les onglets inactifs, afin de ne pas épuiser la batterie de votre appareil mobile si vous le laissez ouvert en arrière-plan.

Nicholas Zakas a écrit un article extrêmement détaillé et instructif. article sur requestAnimationFrame sur son blog qui vaut la peine d'être lu. Si vous souhaitez obtenir des instructions de mise en œuvre précises et rapides, alors Paul Irish a écrit une shim requestAnimationFrame - C'est ce que j'ai utilisé dans toutes les applications Canvas que j'ai créées jusqu'à récemment.

ACTUELLEMENT

Encore mieux que l'utilisation de requestAnimationFrame à la place de setTimeout et setInterval, Joe Lambert a écrit une Nouvelle cale améliorée appelés requestInterval et requestTimeout, dans lesquels il explique les problèmes qui existent lors de l'utilisation de requestAnimFrame. Vous pouvez consulter le Résumé du script. .

ACTUELLEMENT x2

Maintenant que tous les navigateurs ont rattrapé leur retard sur la spécification, il y a eu un mise à jour du polyfill requestAnimFrame() qui restera probablement celle à utiliser pour couvrir tous les vendeurs.

Utilisez plus d'une toile

Une technique pour les jeux à forte intensité d'animation qui @nicolahibbert dont il est question dans un son post sur l'optimisation des jeux Canvas mentionne qu'il peut être préférable d'utiliser plusieurs toiles superposées plutôt que de tout faire dans une seule toile. Nicola explique que "dessiner trop de pixels sur le même canevas en même temps fera chuter votre taux d'images par seconde. Prenez Breakout par exemple. Si vous essayez de dessiner les briques, la balle, la pagaie, les power-ups ou les armes, puis chaque étoile en arrière-plan, cela ne fonctionnera tout simplement pas, car il faut trop de temps pour exécuter chacune de ces instructions à tour de rôle. En divisant le champ d'étoiles et le reste du jeu sur des toiles séparées, vous pouvez garantir un framerate décent."

Rendu des éléments hors écran

J'ai dû faire cela pour quelques applications que j'ai créées, dont les suivantes L'application Facebook du projet génome olympique de Samsung . C'est une chose extrêmement utile à connaître et à utiliser, que ce soit nécessaire ou non. Elle réduit considérablement le temps de chargement, et peut s'avérer très utile pour charger des images hors écran, car elles peuvent parfois prendre du temps.

var tmpCanvas = document.createElement('canvas'),
    tmpCtx = tmpCanvas.getContext('2d'),
    img = document.createElement('img');

img.onload = function() {
    tmpCtx.drawImage(thumbImg, 0, 0, 200, 200);
};
img.src = '/some/image/source.png';

Remarquez que le src de l'image est défini après son chargement. C'est une chose importante à ne pas oublier. Une fois que les images ont été chargées et dessinées dans ces toiles temporaires, vous pouvez les dessiner sur votre toile principale en utilisant la même ctx.drawImage(), mais au lieu de mettre l'image comme premier argument, vous utilisez 'tmpCtx.canvas' pour référencer la toile temporaire.

Autres conseils, astuces et ressources

La toile a une référence arrière

Le contexte 2d a une référence arrière à l'élément DOM qui lui est associé :

var ctx = doc.getElementById('canvas').getContext('2d');
console.log(ctx.canvas);    // HTMLCanvasElement

J'aimerais entendre d'autres personnes à ce sujet. Je travaille à l'élaboration d'une liste des éléments que nous devrions normaliser afin d'ajouter une nouvelle section au site Web de mon entreprise. Normes et meilleures pratiques en matière de code frontal . J'aimerais avoir le plus de réactions possible à ce sujet.

17voto

jaredwilli Points 2215

Redessiner les régions

La meilleure technique d'optimisation du canevas pour les animations consiste à limiter la quantité de pixels qui sont effacés/peints à chaque image. La solution la plus facile à mettre en œuvre consiste à réinitialiser l'ensemble de l'élément de toile et à tout redessiner, mais cette opération est coûteuse à traiter pour votre navigateur.

Réutilisez autant de pixels que possible entre les images. Cela signifie que moins il y a de pixels à traiter à chaque image, plus votre programme sera rapide. Par exemple, lorsque vous effacez des pixels à l'aide de la méthode clearRect(x, y, w, h), il est très utile d'effacer et de redessiner uniquement les pixels qui ont changé et non la totalité du canevas.

Sprites procéduraux

Générer des graphiques de manière procédurale est souvent la solution, mais parfois ce n'est pas la plus efficace. Si vous dessinez des formes simples avec des remplissages solides, la meilleure façon de procéder est de les dessiner de manière procédurale. Mais si vous dessinez des entités plus détaillées avec des traits, des remplissages en dégradé et d'autres éléments sensibles aux performances, il est préférable d'utiliser des sprites d'image.

Il est possible de s'en sortir avec un mélange des deux. Dessinez les entités graphiques de manière procédurale sur le canevas une fois au démarrage de votre application. Ensuite, vous pouvez réutiliser les mêmes sprites en peignant des copies de ceux-ci au lieu de générer les mêmes ombres portées, dégradés et traits de manière répétée.

Pile d'états et transformation

Le canevas peut être manipulé par des transformations telles que la rotation et la mise à l'échelle, ce qui entraîne une modification du système de coordonnées du canevas. C'est là qu'il est important de connaître la pile d'états pour laquelle deux méthodes sont disponibles : context.save() (pousse l'état actuel vers la pile) et context.restore() (revient à l'état précédent). Cela vous permet d'appliquer une transformation à un dessin, puis de revenir à l'état précédent pour vous assurer que la forme suivante n'est pas affectée par une transformation antérieure. Les états comprennent également des propriétés telles que les couleurs de remplissage et de contour.

Compositing

Les modes de composition, qui permettent notamment le masquage et la superposition, constituent un outil très puissant pour travailler avec le canevas. Il existe un large éventail de modes de composition disponibles, qui sont tous définis par la propriété globalCompositeOperation du contexte de la toile. Les modes composites font également partie des propriétés d'empilement d'états, de sorte que vous pouvez appliquer une opération composite, empiler l'état et en appliquer une autre, puis revenir à l'état antérieur où vous avez effectué la première opération. Cela peut être particulièrement utile.

Anti-aliasing

Pour permettre des dessins sous-pixel, toutes les implémentations de canevas dans les navigateurs utilisent l'anticrénelage (bien que cela ne semble pas être une exigence de la spécification HTML5). L'anticrénelage peut être important à garder à l'esprit si vous voulez dessiner des lignes nettes et que vous remarquez que le résultat semble flou. Cela se produit parce que le navigateur interpole l'image comme si elle se trouvait réellement entre ces pixels. Il en résulte une animation beaucoup plus fluide (vous pouvez réellement vous déplacer d'un demi-pixel par mise à jour) mais vos images paraîtront floues.

Pour contourner ce problème, vous devrez soit arrondir à des valeurs entières, soit décaler d'un demi-pixel, selon que vous dessinez des remplissages ou des traits.

Utilisation de nombres entiers pour les positions x et y de drawImage()

Si vous appelez drawImage sur l'élément Canvas, le processus est beaucoup plus rapide si vous arrondissez les positions x et y à un nombre entier.

Voici un cas de test sur jsperf montrant combien l'utilisation des nombres entiers est plus rapide que celle des nombres décimaux.

Arrondissez donc vos positions x et y à des nombres entiers avant d'effectuer le rendu.

Plus rapide que Math.round()

Un autre test de jsperf montre que Math.round() n'est pas nécessairement la méthode la plus rapide pour arrondir les nombres. L'utilisation d'un hack bit à bit s'avère en fait plus rapide que la méthode intégrée.

Optimisation du Sprite Canvas

Nettoyer le canevas

Pour vider le canevas de tous les pixels existants, on utilise généralement context.clearRect(x, y, w, h), mais il existe une autre option. Chaque fois que la largeur et la hauteur du canevas sont définies, même si elles ont la même valeur à plusieurs reprises, le canevas est réinitialisé. Il est bon de le savoir lorsque vous travaillez avec un canevas de taille dynamique, car vous remarquerez que des dessins disparaissent.

Distribution des calculs

Le profileur de Chrome Developer Tools est très utile pour déterminer quels sont les goulets d'étranglement en matière de performances. En fonction de votre application, vous devrez peut-être remanier certaines parties de votre programme pour améliorer les performances et la façon dont les navigateurs traitent certaines parties de votre code.

Techniques d'optimisation

2voto

jaredwilli Points 2215

Après avoir travaillé sur un une application Facebook récemment lancée qui utilise Canvas et les informations du profil Facebook des utilisateurs (la quantité de données qu'elle doit traiter est énorme pour certains) pour vous faire correspondre, vous et vos amis qui utilisent également l'application, aux athlètes olympiques, comme un type de séparation à 6 degrés, il y a beaucoup de choses que j'ai apprises dans mes efforts considérables pour faire tout ce que je pouvais essayer pour améliorer les performances dans l'application.

J'ai littéralement passé des mois et des jours à travailler à la refonte du code que je connaissais déjà si bien et que je croyais être la meilleure façon de faire les choses.

Utilisez les éléments du DOM chaque fois que cela est possible

Le fait est que les navigateurs ne sont toujours pas prêts à gérer des applications plus intensives dans Canvas, surtout si vous devez développer l'application en prenant en charge IE 8. Il existe parfois des cas où le DOM est plus rapide que l'implémentation actuelle de l'API Canvas au moment où nous écrivons ces lignes. C'est du moins ce que j'ai constaté en travaillant sur une application html5 et canvas animée d'une page unique extrêmement complexe pour Samsung.

Nous avons réussi à améliorer les performances tout en continuant à utiliser Canvas pour effectuer un travail complexe de découpage d'images en cercles, ce qui n'aurait probablement pas posé de problème si nous avions continué à le faire.

Quelques jours avant le lancement, nous avons décidé d'essayer une technique différente, et plutôt que de créer des toiles temporaires hors écran qui étaient placées sur la toile visible une fois recadrées en cercles, etc., nous avons simplement ajouté des éléments Image DOM sur la toile, en utilisant les coordonnées x et y que nous avions utilisées pour placer les toiles temporaires auparavant.

Pour le découpage des images en cercles, c'était simple, nous avons simplement utilisé la propriété CSS3 border-radius pour le faire, ce qui représentait beaucoup moins de travail que la série complexe de changements d'état et l'utilisation ingénieuse et créative, mais excessive, de la méthode .clip().

Une fois qu'elles sont placées dans le DOM, l'animation des images se produit, et les nœuds DOM de chaque image sont animés comme des entités distinctes du Canvas. Des entités sur lesquelles nous pouvons avoir un contrôle total du style grâce aux CSS.

Cette technique est similaire à une autre méthode pour réaliser ce type de travail qu'il est bon de connaître également, qui consiste à superposer les toiles les unes sur les autres, plutôt que de les tirer vers un même contexte.

2voto

Shamaila Tahir Points 723

Voici mes conseils

1) Utilisez clearRect pour effacer le canevas au lieu de canvas.width=canvas.width, car ce dernier réinitialise les états du canevas

2) Si vous utilisez les événements de souris sur le canevas, utilisez la fonction suivante, elle est fiable et fonctionne dans la plupart des cas.

/**  returns the xy point where the mouse event was occured. 
 @param ev The event object.
*/
function getXY(ev){
   return getMousePosition(ev, ev.srcElement || ev.originalTarget);
}

 /**  returns the top-left point of the element
       @param elem The element
   */
function getElementPos(elem){
   var obj = elem;
   var top = 0;
   var left = 0;
    while (obj && obj.tagName != "BODY") {
      top += obj.offsetTop-obj.scrollTop;
      left += obj.offsetLeft -obj.scrollLeft ;
      obj = obj.offsetParent;
     }
  return {
    top: top,
    left: left
    };
};

/**  returns the xy point where the mouse event was occured inside an element. 
@param ev The event object.
 @param elem The element
*/
function getMousePosition(evt, elem){
var pageX, pageY;
if(typeof(window.pageYOffset)=='number') {
    pageX=window.pageXOffset;
    pageY=window.pageYOffset;
}else{
    pageX=document.documentElement.scrollLeft;
    pageY=document.documentElement.scrollTop;
}
var mouseX = evt.clientX - getElementPos(elem).left + pageX;
var mouseY = evt.clientY - getElementPos(elem).top + pageY;
return {
    x: mouseX,
    y: mouseY
};
};

3) Utilisez ExplorerCanvas si vous voulez supporter IE7

4) Au lieu de nettoyer toute la toile, ne nettoyez que la partie qui doit l'être. C'est bon pour la performance.

1voto

jaredwilli Points 2215

Voici d'autres conseils et suggestions que j'ai rassemblés dans une liste hier soir et qui méritent d'être partagés.

  • N'incluez pas jQuery, à moins que vous n'ayez besoin d'en faire plus que de sélectionner l'icône de l'appareil. <canvas> .

    J'ai réussi à m'en passer pour presque tout ce que j'ai fait en toile.

  • Créer des fonctions abstraites y découpler votre code . Séparer la fonctionnalité de l'apparence ou de l'état initial du dessin.

    Rendez les fonctions communes réutilisables autant que possible. Idéalement, vous devriez utiliser un modèle de module, qui vous permet de créer un objet utils contenant les fonctions communes.

  • Utilisez des noms de variables à une ou deux lettres lorsque cela est utile. ( x, y, z ).

    Le système de coordonnées dans Canvas ajoute plus de lettres simples qui sont communément déclarées comme variables. Ce qui peut conduire à la création de plusieurs variables simples/doubles ( dX, dY, aX, aY, vX, vY ) comme partie d'un élément.

    Je vous suggère de taper ou d'abréger le mot ( dirX, accelX, velX ) ou être descriptif, sinon les choses pourraient devenir assez confuses pour vous par la suite, croyez-moi.

  • Créez des fonctions de construction qui peuvent être invoquées selon les besoins pour créer des éléments de jeu. Vous pouvez ajouter des méthodes et des propriétés personnalisées au sein du constructeur, et en créer autant que vous le souhaitez ; ils auront tous leurs propres propriétés et méthodes.

    Exemple d'une fonction de construction de boule que j'ai faite :

    // Ball constructor
    var Ball = function(x, y) {
        this.x = x;
        this.y = y;
    
        this.radius = 10;
        this.color = '#fff';
    
        // Direction and min, max x,y
        this.dX = 15;
        this.dY = -15;
    
        this.minX = this.minY = 20 + this.radius;
        this.maxX = this.radius - (canvasWidth - 20);
        this.maxY = this.radius + canvasHeight;
    
        this.draw = function(ctx) {
            ctx.beginPath();
                ctx.arc(this.x, this.y, this.radius, 0, twoPI, true);
            ctx.closePath();
            ctx.save();
                ctx.fillStyle = this.color;
                ctx.fill();
            ctx.restore();
        };
    };

Création de la balle

ball = new Ball(centerX, canvasHeight - paddle.height - 30);
ball.draw(ctx);
  • Une bonne base de travail consiste à créer 3 fonctions : init() - fait tout le travail initial, et configure les variables de base et les gestionnaires d'évènements etc... draw() - appelée une fois pour commencer le jeu et dessine la première image du jeu, y compris la création des éléments qui peuvent changer ou avoir besoin d'être construits. update() - appelé à la fin de draw() et en lui-même via requestAnimFrame. Met à jour les propriétés des éléments qui changent, ne faites que ce que vous avez besoin de faire ici.

  • Effectuer le moins de travail possible à l'intérieur de la boucle, en effectuant des mises à jour des parties ou des éléments qui changent. . Créez les éléments du jeu et effectuez tout autre travail d'interface utilisateur en dehors de la boucle d'animation.

    La boucle d'animation est souvent une fonction récursive, ce qui signifie qu'elle s'appelle rapidement et de manière répétée pendant l'animation pour dessiner chaque image.

    Si de nombreux éléments sont animés en même temps, vous pouvez d'abord créer les éléments à l'aide d'une fonction de constructeur, si ce n'est pas déjà le cas, et ensuite, dans le constructeur, créer une méthode "timer" avec requestAnimFrame/setTimeout en l'utilisant comme vous le feriez normalement dans une boucle d'animation, mais en n'affectant spécifiquement que cet élément.

    Vous pourriez faire en sorte que chaque élément du jeu ait ses propres méthodes de minuterie, de dessin et d'animation. dans le constructeur.

    Cela vous permet de séparer totalement le contrôle de chaque élément et une grande boucle d'animation ne sera pas du tout nécessaire puisque la boucle est divisée en plusieurs éléments et que vous pouvez la démarrer et l'arrêter à volonté.

Ou une autre option :

  • Créer une fonction constructeur Timer() que vous pouvez utiliser et donner à chaque élément d'animation individuellement, minimisant ainsi la charge de travail dans les boucles d'animation

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X