I define my canvas as you normally would, but I also include a bunch of different makeshift canvases:

var graphicOne = document.createElement("canvas");
var graphicOneDraw = graphicOne.getContext("2d");

// etc... (eg. graphicTwo)

Then I render all of these canvases, like so:

function preRender() {
  graphicOne.width = 24;
  graphicOne.height = 24;

  graphicOneDraw.fillStyle = '#0000ff';
  graphicOneDraw.arc(12, 12, 12, 0, Math.PI * 2, true);

  // etc... (eg. graphicTwo)

In the main loop, my game logic is called before I render it on canvas:

ctx.drawImage(graphicOne, player.x - 12, player.y - 12);

// etc... (eg. graphicTwo) and other things that needs to be rendered

The graphics I need to render is actually more CPU/GPU intensive; this is just a bare bones example so you get the gist of it. But over to the question: Is there a better way to go about this?

  • 1
    \$\begingroup\$ Hello, wellcome to Code Review, please provide a complete working example with all the code. \$\endgroup\$ – Mario Santini Apr 9 '18 at 7:31
  • \$\begingroup\$ @MarioSantini I've provided all the information necessary - no need to over-complicate the question. What's unclear to you? \$\endgroup\$ – Will Pierce Apr 9 '18 at 7:38
  • 1
    \$\begingroup\$ @JasonOliver Code Review is about reviewing your complete, working code. We generally don't (shouldn't) review partial code, example code, or stub code. Please see our help center for more information. \$\endgroup\$ – Dannnno May 10 '18 at 13:32

JIT content rendering

There is no observable difference in performance (after setting up) between using an image or a canvas to render context. As rendering content can be many times faster than downloading content it is a very good method of increasing performance at startup.

Some warnings.

Clean up state

Using the 2D context requires that its state be maintained. Be sure to clean the state when you no longer have a need as the state can occupy a lot of memory.


ctx.filter = url("#element-id"); // can consume lots of memory and if animated ongoing CPU/GPU resources
ctx.fillStyle = ctx.createPattern(ctx.canvas,"repeat"); // requires copy of source image
ctx.strokeStyle = ctx.createPattern(image,"repeat"); // requires copy of source image
ctx.font = "128px myFancyFont"; // a loaded font

ctx.beginPath(); // lets create a very long path
for(var i = 0; i < 10000; i ++) { ctx.lineTo(Math.random() * 100, Math.random() * 100) }
ctx.stroke();  // You may be done with the path but its still there  
               // ready for another stroke or fill

All the above (and there are more) require considerable memory resources. They also prefer these resources to be readily available to the GPU so if possible will occupy GPU RAM (shadowed from RAM)

Running out of free GPU RAM is a the biggest performance killer for the browser's rendering engines as it begins to swap resources in and out of GPU RAM to complete the render (GPU bus thrashing, or GPU RAM thrashing). Do your best to keep usage as low as possible by always releasing unneeded resources.

I would make some changes

function createAndRenderResource(renderFunc, ...args)
    const img = document.createElement("canvas");
    const ctx = img.getContext("2d");
    img.width = 24;
    img.height = 24;
    ctx.save(); // save default low memory state
    renderFunc(ctx, ...args); // add the content
    ctx.restore(); // cleanup releasing any unneeded RAM or connected processes
    return img; // you can not remove the 2D context once made
                // it remains in memory

function render(ctx, color) {
    ctx.fillStyle = color;
    ctx.arc(12, 12, 12, 0, Math.PI * 2, true);

const images = [

Powers of two

The canvas size can be anything, an image can be anything. But the way GPU hold images is optimized for very fast access. Images are always stored in sizes that are powers of 2 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096... etc

To know the memory foot print of any resource within the GPU find the nearest power of two above the width or height.

For example a 48 by 12 (576 pixels) image will occupy, nearest width is 64, and nearest height is 16 64 by 16 (1024 pixels) almost double the amount of Ram needed to hold the image in GPU RAM.

Dead pixels.

The code you have given creates a canvas that is 24 by 24 pixels. The memory footprint on the GPU is thus 32 by 32 pixels. 77% per pixel dead (unused) RAM.

You can make things better by using a single canvas and drawing multiple sprites to that. A canvas 128 by 128 can hold 25 sprites that are 24 by 24 and have room for a one pixel boundary between them to stop pixel bleeding due to linear lookups. This reduces the dead pixel rate to ~14%

Note: the same power of two rule applies to Images.

This will only effect your code if you are getting close to the GPU memory limits imposed by the browser. The more dead pixels the quicker you get to that limit for the same resources.

For large numbers of variable sized sprites use a single canvas near a power of two size I use a sprite lookup as a property, attached to the image / canvas being used, that stores the position and size of each sprite.

// a sprite is {x,y,w,h} the position on the sprite sheet.
// sprites is an array of sprites. and spriteIndex is which to render
function drawSprite(img, spriteIndex, x,y){
    const spr = img.sprites[spriteIndex];
    ctx.drawImage(img, spr.x, spr.y, spr.w, spr.h, x, y, spr.w, spr.h);

BTW you break the codeReview site rules by not supplying working code, you have given example of what your working code does. It is incomplete and non functioning.

| improve this answer | |
  • 1
    \$\begingroup\$ Great answer with no up votes. CR.SE is a strange place. Thanks for putting efforts into preparing high quality answer! \$\endgroup\$ – Igor Soloydenko May 10 '18 at 4:42

Not the answer you're looking for? Browse other questions tagged or ask your own question.