Improving canvas pre-rendering
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. 

eg 

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 &lt; 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.beginPath();
    ctx.fillStyle = color;
    ctx.arc(12, 12, 12, 0, Math.PI * 2, true);
    ctx.fill();
}


const images = [
     createAndRenderResource(render,"red"),
     createAndRenderResource(render,"green"),
     createAndRenderResource(render,"blue"),
]


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.