Optimizing my JavaScript Canvas Game Pt 2: Canvas Doubles
I’m writing about cleaning up and optimizing my JavaScript game portfolio. Part one dealt with removing jquery and only animating when absolutely necessary. In this chapter, I’ll be doing a fancy little canvas caching trick, to save on unnecessary redrawing of the canvas map. Lets look at another potential offender in the Timeline.
Render is taking almost 8ms, about half of the coveted 16.6ms for the clean 60 FPS standard. Let’s see if we can do something about that.
function renderMap(ctx) {
var x, y, cell;
for (y = 0; y < MAP.th; y++) {
for (x = 0; x < MAP.tw; x++) {
cell = tcell(x, y);
if (cell) {
ctx.fillStyle = COLORS[cell - 1];
ctx.fillRect(x * TILE, y * TILE, TILE, TILE);
}
}
}
}
renderMap looks to be taking most of that time. And you can see why, loops in loops. I’m taking every cell from my map JSON and painting the whole canvas every frame. While rendering the image on the canvas seems to be a necessary evil, there’s a little work around we can use to render that point moot.
As pointed out to me by Thomas Hunter at a coding meet up, I shouldn’t be drawing what’s already been drawn. So how do I draw the map once and have it stay put even while calling clearRect() to update the player position?
<canvas id="canvas" class="canvas canvasLower"></canvas>
<canvas class="canvas upperCanvas" id="canvas2"></canvas>
Boom. A SECOND CANVAS. Just draw the map once on the upper canvas, and don’t render it on the lower canvas where your player will be jumping around and clearing Rects. Your update will still be applying your physics to the lower canvas, you just won’t be drawing them.
My mother would call this lazy, I call it efficient. Let’s make it happen. With the HTML ready let’s prep our JS.
function drawMapOnce(){
// create and save the map image
mapCache = document.getElementById('canvas2');
cachedContext = mapCache.getContext("2d");
mapCache.width = canvas.width;
mapCache.height = canvas.height;
renderMap(cachedContext);
ctx.drawImage( mapCache, 0, 0 );
}
- Get that second canvas, give it a 2d context
- Make it the same size as the first canvas
- RenderMap just like old times on that cachedContext
- On the original context (the top canvas) draw the image of that cachedContext
Note: you could also save the image as a png here with canvas.toDataURL(); and then load the image on top of the canvas, but because this is my portfolio, I expect to be changing it often and this makes it slightly more extendable.
Almost done! But right down we have two canvas elements, one above and one below, positioning block style. Let’s make one sit on top in CSS.
.canvasLower{
z-index: 0;
position: absolute;
}
.canvasUpper{
z-index: 2;
position: absolute;
}
Done! Let’s admire our work on the Timeline.
Ow! I nearly broke my finger scrolling to zoom in far enough on the Timeline. But now we’re running the entire JavaScript in 0.82ms each frame. Down from my original 40.8ms.
Of course, I see now that I’m drawing those headlines in each frame, which could be easily packed into the drawMapOnce function. This is the benefit of reflecting on your code and writing a medium post about it. But I’m definitely reaching my goal of a consistent 60FPS, and rather than focusing on that tiny little improvement, I should try to find bigger fish to fry. Which for me would be to smooth out the initial loading of the page.
Keep coding, keep reflecting, and make prioritized choices about optimizations!
Source code on Github