Canvas drawImage slow first time another canvas is used as the source argument

Viewed 766

I am seeing slow canvas drawing the first time I use another canvas as the drawing source. Subsequent canvas to canvas .drawImage calls are fine until I swap images (and then I see the same issue again).

Sample code below - an image is loaded and then 4 canvases are created, the 1st canvas is draw from the image itself, the 2nd canvas is drawn from the 1st, etc. After the canvases are created the source image is swapped and the code run again.

        var sourceImage = new Image();  // Original image
        var myImages = []; // Array of image and canvases references
        myImages[0] = sourceImage; // Set first myImage to image source

        // Image onload 
        sourceImage.onload = function () {
     
            console.log("Imageload", new Date() - t0);
            myImages[0] = sourceImage;

            // Loop to create and draw on canvases
            for (var i = 1; i <= 4; i += 1) {

                // Create canvas
                myImages[i] = document.createElement("canvas");

                // Set canvas dimensions to same as original image
                myImages[i].width = myImages[0].width;
                myImages[i].height = myImages[0].height;

                // Draw last canvas / image onto this canvas
                t0 = new Date();
                myImages[i].getContext("2d").drawImage(
                    myImages[i - 1],
                    0,
                    0,
                    myImages[i].width,
                    myImages[i].height
                );
                console.log("drawImage", i,  new Date() - t0); 
                
            }

            // Finished with black.jpg so load white.jpg           
            if (sourceImage.getAttribute("src") == "images/black.jpg") {
                sourceImage.src = "images/white.jpg"
            }

        }

        // Load image
        t0 = new Date();
        sourceImage.src = "images/black.jpg"

The console output is ...

Imageload 36
drawImage 1 0
drawImage 2 255
drawImage 3 0
drawImage 4 0

Imageload 35
drawImage 1 0
drawImage 2 388
drawImage 3 1
drawImage 4 1

My question is why is the 2nd canvas drawing slow? I have tried various image files, and different canvas sizes but always see the same outcome. I have tested on Chrome and Safari.

If the slow draw was on the first canvas I could accept that although the .onload fired there was still something going on with the image. But the slowness is on the second canvas i.e. the first has been drawn from the image without issue.

1 Answers

I think you just hit a weird optimization quirk here and it might be quite hard to have a definitive answer as to what happens, but I'll try to make an educated guess anyway.


It seems the browser comes back to the CPU thread before the GPU actually has executed the job it was assigned, taking full advantage of tasks parallelism.

So in the first loop, the GPU will start a job asking to draw the <img> to the <canvas>, this image's data which even though is decoded in Chrome, still has to be transfered to the GPU and converted to an actual bitmap.
But as we said, this is done in parallel and thus the js script can continue and proceed with the second loop directly while doing this job.

However when it comes at drawing the first target canvas on the second, it will see that there is one running GPU job that will modify it, and will thus block the CPU thread until the first drawing has completed.

The next iterations though will only deal with <canvas> sources which bitmap buffers are already on the GPU, so they won't take any relevant time.

We can somehow confirm this by simply waiting a few ms between each iterations. Doing so, all canvas-to-canvas operations will take about 0ms.

var url1 = "https://upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png?bar" + Math.random();
var url2 = "https://upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png?foo" + Math.random();

var sourceImage = new Image(); // Original image
sourceImage.crossOrigin = true;
var myImages = []; // Array of image and canvases references
myImages[0] = sourceImage; // Set first myImage to image source

// Image onload 
sourceImage.onload = async function() {

  console.log("Imageload", new Date() - t0);
  myImages[0] = sourceImage;
  // create canvases before hand to be sure it's not part of the issue
  for (var i = 1; i <= 4; i += 1) {
    // Create canvas
    myImages[i] = document.createElement("canvas");

    // Set canvas dimensions to same as original image
    myImages[i].width = myImages[0].width;
    myImages[i].height = myImages[0].height;
    myImages[i].getContext("2d");
  }

  // Loop to create and draw on canvases
  for (var i = 1; i <= 4; i += 1) {
    // Draw last canvas / image onto this canvas
    t0 = new Date();
    myImages[i].getContext("2d").drawImage(
      myImages[i - 1],
      0,
      0,
      myImages[i].width,
      myImages[i].height
    );
    console.log("drawImage", i, new Date() - t0);
    await new Promise(r => setTimeout(r, 500));

  }

  // Finished with black.jpg so load white.jpg           
  if (sourceImage.getAttribute("src") == url1) {
    sourceImage.src = url2
  }

};

// Load image
t0 = new Date();
sourceImage.src = url1;

Similarly, if we do generate ImageBitmaps of each source, we can see that the one taking the most time is as expected the <img>:

var url1 = "https://upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png?bar" + Math.random();
var url2 = "https://upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png?foo" + Math.random();

var sourceImage = new Image(); // Original image
sourceImage.crossOrigin = true;
var myImages = []; // Array of image and canvases references
myImages[0] = sourceImage; // Set first myImage to image source

// Image onload 
sourceImage.onload = async function() {

  console.log("Imageload", new Date() - t0);
  myImages[0] = sourceImage;
  // create canvases beforehand to be sure it's not part of the issue
  for (var i = 1; i <= 4; i += 1) {
    // Create canvas
    myImages[i] = document.createElement("canvas");

    // Set canvas dimensions to same as original image
    myImages[i].width = myImages[0].width;
    myImages[i].height = myImages[0].height;
    myImages[i].getContext("2d");
  }

  // Loop to create and draw on canvases
  for (var i = 1; i <= 4; i += 1) {
    // wait for create ImageBitmap to be created
    t0 = new Date();
    const img = await createImageBitmap(myImages[i - 1]);
    console.log("createImageBitmap", i, new Date() - t0);

    t0 = new Date();
    myImages[i].getContext("2d").drawImage(
      img,
      0,
      0,
      myImages[i].width,
      myImages[i].height
    );
    console.log("drawImage", i, new Date() - t0);

  }

  // Finished with black.jpg so load white.jpg           
  if (sourceImage.getAttribute("src") == url1) {
    sourceImage.src = url2
  }

};

// Load image
t0 = new Date();
sourceImage.src = url1;


Ps: one might be tempted to call getImageData to force coming back to the CPU thread synchronously, but doing so we also transfer all the canvases bitmaps back and forth between CPU and GPU, creating actually the same slowness at every loops.

Related