HTML5 Canvas Performance and Optimization Tips, Tricks and Coding Best Practices

30,437

Solution 1

Redraw Regions

The best canvas optimization technique for animations is to limit the amount of pixels that get cleared/painted on each frame. The easiest solution to implement is resetting the entire canvas element and drawing everything over again but that is an expensive operation for your browser to process.

Reuse as many pixels as possible between frames. What that means is the fewer pixels that need to be processed each frame, the faster your program will run. For example, when erasing pixels with the clearRect(x, y, w, h) method, it is very beneficial to clear and redraw only the pixels that have changed and not the full canvas.

Procedural Sprites

Generating graphics procedurally is often the way to go, but sometimes that's not the most efficient one. If you're drawing simple shapes with solid fills then drawing them procedurally is the best way do so. But if you're drawing more detailed entities with strokes, gradient fills and other performance sensitive make-up you'd be better off using image sprites.

It is possible to get away with a mix of both. Draw graphical entities procedurally on the canvas once as your application starts up. After that you can reuse the same sprites by painting copies of them instead of generating the same drop-shadow, gradient and strokes repeatedly.

State Stack & Transformation

The canvas can be manipulated via transformations such as rotation and scaling, resulting in a change to the canvas coordinate system. This is where it's important to know about the state stack for which two methods are available: context.save() (pushes the current state to the stack) and context.restore() (reverts to the previous state). This enables you to apply transformation to a drawing and then restore back to the previous state to make sure the next shape is not affected by any earlier transformation. The states also include properties such as the fill and stroke colors.

Compositing

A very powerful tool at hand when working with canvas is compositing modes which, amongst other things, allow for masking and layering. There's a wide array of available composite modes and they are all set through the canvas context's globalCompositeOperation property. The composite modes are also part of the state stack properties, so you can apply a composite operation, stack the state and apply a different one, and restore back to the state before where you made the first one. This can be especially useful.

Anti-Aliasing

To allow for sub-pixel drawings, all browser implementations of canvas employ anti-aliasing (although this does not seem to be a requirement in the HTML5 spec). Anti-aliasing can be important to keep in mind if you want to draw crisp lines and notice the result looks blurred. This occurs because the browser will interpolate the image as though it was actually between those pixels. It results in a much smoother animation (you can genuinely move at half a pixel per update) but it'll make your images appear fuzzy.

To work around this you will need to either round to whole integer values or offset by half a pixel depending on if you're drawing fills or strokes.

Using Whole Numbers for drawImage() x and y positions

If you call drawImage on the Canvas element, it's much faster if you round the x and y position to a whole number.

Here's a test case on jsperf showing how much faster using whole numbers is compared to using decimals.

So round your x and y position to whole numbers before rendering.

Faster than Math.round()

Another jsperf test shows that Math.round() is not necessarily the fastest method for rounding numbers. Using a bitwise hack actually turns out to be faster than the built in method.

Canvas Sprite Optimization

Clearing the Canvas

To clear the entire canvas of any existing pixels context.clearRect(x, y, w, h) is typically used – but there is another option available. Whenever the width/height of the canvas are set, even if they are set to the same value repeatedly, the canvas is reset. This is good to know when working with a dynamically sized canvas as you will notice drawings disappearing.

Computation Distribution

The Chrome Developer Tools profiler is very useful for finding out what your performance bottlenecks are. Depending on your application you may need to refactor some parts of your program to improve the performance and how browsers handle specific parts of your code.

Optimization techniques

Solution 2

Here's some more tips and suggestions I put into a list last night worth sharing.

  • Don't include jQuery unless you need to do more than just selecting the <canvas>.

    I've managed to get by without it for almost everything I've made in canvas

  • Create abstracted functions and decouple your code. Separate functionality from appearance or initial draw state.

    Make common functions reusable as much as possible. Ideally, you should use a module pattern, which you can create a utils object that contains common functions.

  • Use single and double letter variable names when it makes sense (x, y, z).

    The coordinate system in Canvas adds more single letters that are commonly declared as variables. Which can lead to creating multiple single/double variables (dX, dY, aX, aY, vX, vY) as part of an element.

    I suggest you type out or abbr. the word (dirX, accelX, velX) or be descriptive, otherwise things could get pretty confusing for you later on, trust me.

  • Create constructor functions which can be invoked as needed for making game elements. You can add custom methods and properties within the constructor, and create any number of you may need and they all will have their own properties and methods.

    Example of a Ball constructor function I made:

    // 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();
        };
    };
    

Creating the Ball

ball = new Ball(centerX, canvasHeight - paddle.height - 30);
ball.draw(ctx);
  • A good base to work with is to create 3 functions: init() - do all the initial work, and setup the base vars and event handlers etc... draw() - called once to begin the game and draws the first frame of the game, including the creation of elements that may be changing or need constructing. update() - called at the end of draw() and within itself via requestAnimFrame. Updates properties of changing elements, only do what you need to do here.

  • Do the least amount of work within the loop, making updates to the changing parts or elements. Create the game elements do any other UI work outside the animation loop.

    The animation loop is often a recursive function, meaning it calls itself rapidly and repeatedly during the animation to draw each frame.

    If there are many elements being animated at once, you might want to first create the elements using a constructor function if your not already, and then within the constructor make a 'timer' method that has requestAnimFrame/setTimeout using it just how you would normally within any animation loop, but effects this element specifically only.

    You could make each game element have it's own timer, draw, and animate methods in the constructor.

    Doing this gives you full separation of control for each element and one big animation loop will not be necessary at all since the loop is broken up into each element and you start/stop at will.

Or another option:

  • Create a Timer() constructor function which you can use and give each animating element individually, thereby minimizing work load within animation loops

Solution 3

Here are my tips

1) Use clearRect to clear the canvas instead of canvas.width=canvas.width, because later resets the canvas states

2) If you are using mouse events on the canvas use following function, its is reliable and works in most of the cases.

/**  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) Use ExplorerCanvas if you want to support IE7

4) Instead of clearing the whole canvas clear only the part which is needed to be cleaned. Its good for performance.

Solution 4

After having worked on a recently launched Facebook app that uses Canvas and users Facebook profile information (the amount of data it must accommodate is massive for some) to match you and friends of yours also using the app, to Olympic athletes like a 6 degrees of separation type of thing, there's quite a lot I have learned in my extensive efforts to do everything I could possibly try for increasing performance within the app.

I literally spent months, and days at a time just working to re-factor the code which I knew already so well, and believed it to be the most optimal way to do things.

Use DOM Elements Whenever Possible

The fact is, browsers are still just not ready to handle more intensive running applications in Canvas, especially if you're required to develop the app with support for IE 8. There are sometimes cases where the DOM is faster than the current implementation of the Canvas API at the time of writing this. At least I've found it to be while working on a massively complex single page animating html5 and canvas application for Samsung.

We were able to do quite well at improving the performance of things while still using Canvas to do some complex work to crop images into circles, which would've probably been ok to stick with how we were doing it.

Days before the launch, we decided to try a different technique, and rather than create temporary canvases off-screen which were placed on the visible canvas once cropped into circles etc.., we just appended Image DOM elements on the Canvas, using the x and y coordinates that we had been using for placing the temp canvases before.

For cropping the images into circles, well that was simple, we just used the CSS3 border-radius property to do it which was far less work than the complex series of state changes and while ingenious and creative yet over-use of the .clip() method.

Once they are placed in the DOM, the animation of images the occurs, and the DOM nodes for each image are animated as separate entities of the Canvas. Ones that we can have full control over the styling off easily through CSS.

This technique is similar to another method for doing this type of work that is quite good to know as well, which involves layering Canvases on top of each other, rather than draw them to one context.

Share:
30,437
jaredwilli
Author by

jaredwilli

Google Developer Expert for Chrome DevTools. Senior Front End Engineer for Cisco CloudLock.

Updated on July 30, 2020

Comments

  • jaredwilli
    jaredwilli almost 4 years

    DO YOU KNOW SOME MORE BEST PRACTICES FOR CANVAS??

    Please add to this thread what you know, have learned, or have read online any and all Canvas best practices, tips/tricks for performance

    With Canvas being still very new to internet, and no signs of it ever getting old that I can see in the future, there are not too many documented "best practices" or other really important tips that are a 'must know' for developing with it in any one particular place. Things like this are scattered around and many times on lesser known sites.

    There's so many things that people need to know about, and still so much to learn about too.


    I wanted to share some things to help people who are learning Canvas and maybe some who already know it quite well and am hoping to get some feedback from others about what they feel are some best practices or other tips and tricks for working with Canvas in HTML5.

    I want to start off with one I personally found to be quite a useful yet surprisingly uncommon thing for developers to do.

    1. Indent your code

    Just as you would any other time, in any other language whatever the case may be. It has been a best practice for everything else, and I have come to find that in a complex canvas app, things can get a little confusing when dealing with several different contexts and saved/restore states. Not to mention the code is just more readable and overall cleaner looking too.

    For example:

    ...
    // 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();
    ...
    

    Is the IF statement not easier and cleaner to read and know what is what immediately going on than the ELSE statement in this? Can you see what I'm saying here? I think this should be a method that developers should continue to practice just as they would when writing plain 'ol javascript or any other language even.

    Use requestAnimationFrame instead of setInterval / setTimeout

    setInterval and setTimeout were never intended to be used as animation timers, they're just generic methods for calling functions after a time delay. If you set an interval for 20ms in the future, but your queue of functions takes longer than that to execute, your timer won't fire until after these functions have completed. That could be a while, which isn't ideal where animation is concerned. RequestAnimationFrame is a method which tells the browser that an animation is taking place, so it can optimize repaints accordingly. It also throttles the animation for inactive tabs, so it won't kill your mobile device's battery if you leave it open in the background.

    Nicholas Zakas wrote a hugely detailed and informative article about requestAnimationFrame on his blog which is well worth reading. If you want some hard and fast implementation instructions, then Paul Irish has written a requestAnimationFrame shim – this is what I've used in every one of the Canvas apps I have made until recently.

    ACTUALLY

    Even better than using requestAnimationFrame in place of setTimeout and setInterval, Joe Lambert has written a NEW and improved shim called requestInterval and requestTimeout, which he explains what issues exist when using requestAnimFrame. You can view the gist of the script.

    ACTUALLY x2

    Now that all the browsers have caught up on the spec for this, there has been an update to the requestAnimFrame() polyfill, one which will probably remain the one to use to cover all vendors.

    Use more than one canvas

    A technique for animation-heavy games which @nicolahibbert wrote about in a post of hers on optimizing Canvas games mentions that it may be better to use multiple canvasses layered on top of one another rather than do everything in a single canvas. Nicola explains that "drawing too many pixels to the same canvas at the same time will cause your frame rate to fall through the floor. Take Breakout for example. Trying to draw the bricks, the ball, the paddle, any power-ups or weapons, and then each star in the background – this simply won't work, it takes too long to execute each of these instructions in turn. By splitting the starfield and the rest of the game onto separate canvases, you are able to ensure a decent framerate."

    Render Elements Off-screen

    I have had to do this for a few apps I've made including Samsung's Olympic Genome Project facebook app. It's an extremely useful thing to know and to make use of whether it's needed or not. It decreases load time immensely, plus it can be a really useful technique to load images off screen since they can sometimes take a while.

    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';
    

    Notice that the src of the image is set after it is loaded. This is a key thing to remember to do too. Once the images are done loading and drawn into these temp canvases, you can then draw them to your main canvas by using the same ctx.drawImage(), but instead of putting the image as the first argument, you use 'tmpCtx.canvas' to reference the temp canvas.

    Other tips, tricks and resources

    Canvas has a back-reference

    The 2d context has a back reference to it's associated DOM element:

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

    I'd love to hear more from other people on this. I am working on making a list of things that we should standardize to add a new section to my company's Front-end Code Standards and Best Practices. I'd love to get as much feedback on this as I can.