Search code examples
javascripthtml5-canvasscalingcss-transformsdrawimage

Is CSS Scaling More Efficient than drawImage Scaling?


I would like to know what the difference between scaling with drawImage and CSS is. I have tried both and CSS seems to give slightly smoother results, while drawImage tends to be a little distorted - especially if I am panning the image, blitting from different source x and y over the course of several animation frames. I have drawn this conclusion after testing at many different scale factors. I have a simple example below.

var img = new Image();
var ctx = document.querySelector("canvas").getContext("2d");

img.src = "img1000x1000.png";// width and height are 1000

ctx.canvas.width  = 100;
ctx.canvas.height = 100;
ctx.imageSmoothingEnabled = false;

This runs after img fully loads.

// method 1 uses drawImage to scale
ctx.drawImage(img, 0, 0, 100, 100, 0, 0, 200, 200);

// method 2 draws the img with no scaling and then uses css to scale
ctx.drawImage(img, 0, 0, 100, 100, 0, 0, 100, 100);
ctx.canvas.style.width  = 200;
ctx.canvas.style.height = 200;

Keep in mind, I am not always scaling exactly 2x the size and I am blitting from different areas of the img. I only hand in whole numbers to drawImage to keep things as crisp as possible. I also have image-rendering:pixelated and the other variants of that defined on the canvas in CSS for the CSS scaling to keep things crisp.

I know for my specific example I could just scale the img up once and pan around the final scaled img, but that is not the question. The question is which is more performant. For my application I must scale up an animated tile map on every frame, so I am blitting from multiple sources to a canvas in 1:1 space and then scaling up the final canvas 30 to 60 times per second using requestAnimationFrame.

In the end, they give very similar results. CSS is barely more crisp and true to the original image. Is CSS faster / more efficient? Or is scaling with drawImage more performant?

NOTE / EDIT:

I found that if I'm using standards mode with <!DOCTYPE html> I must pass string values into style.width and style.height for the CSS method. So if you are using standards mode, make sure to set those to "200px" instead of just 200 or your canvas will not scale.


Solution

  • Canvas Alpha makes the difference

    For the best results you should keep the canvas size the same as its resolution. If the canvas does not have any transparent pixels you should turn off alpha.

    Turn off canvas alpha using const ctx = canvas.getContext("2d",{alpha: false})

    Scaling is irrelevant

    For most devices scaling is no more expensive an operation than drawing an unscaled image.

    The cost of rendering comes downs to the number of pixels that are drawn, not the number of pixels in the source.

    Estimating rendering cost

    Cost of drawImage

    If the canvas resolution is 200 by 200 and you draw an image ctx.drawImage that fills it that is 40,000 pixels that need to be rendered.

    Cost of compositing

    If that canvas is scaled via CSS rule, even if alpha is off, the canvas element must be composited to be displayed. The cost will be the number of pixels that the canvas size occupies.

    If the canvas has alpha as true then it must be composited with the background and thus the rendering cost is the display size of the canvas in pixels.

    If the canvas size (CSS) matches the resolution and alpha is off then the canvas does not need to be composited for the final presentation, and there is no additional compositing cost.

    Rendering cost examples

    • canvas is a visible canvas element
    • image is an image that can fit in GPU RAM

    Best with alpha off and no CSS scaling

    canvas.width = canvas.height = 100;
    canvas.style.width = canvas.style.height = "100px";
    const ctx = canvas.getContext("2d", {alpha: false});
    ctx.drawImage(image, 100, 100);
    
    // Total rendering cost 100 * 100 = 10,000 pixels
    

    Scaling via CSS and alpha off

    canvas.width = canvas.height = 200;
    canvas.style.width = canvas.style.height = "100px";
    const ctx = canvas.getContext("2d", {alpha: false});
    ctx.drawImage(image, 200, 200);
    
    // Total rendering cost 200 * 200 + 100 * 100 = 50,000 pixels
    

    Rendering with alpha on, no CSS scaling

    canvas.width = canvas.height = 100;
    canvas.style.width = canvas.style.height = "100px";
    const ctx = canvas.getContext("2d");  // alpha is on
    // same as const ctx = canvas.getContext("2d", {alpha: true});
    ctx.drawImage(image, 100, 100);
    
    // Total rendering cost 100 * 100 + 100 * 100 = 20,000 pixels
    

    Notes

    • There is an unavoidable rendering cost due to the page set up, scrolling and various other factors. These costs are ignored as they can not be avoided.
    • The option to turn of alpha may not be available on all browsers.
    • Software rendering and device capabilities will have the most dramatic effect on rendering performance.