【问题标题】:Canvas drawImage slow first time another canvas is used as the source argumentCanvas drawImage 第一次使用另一个画布作为源参数时很慢
【发布时间】:2020-11-18 03:40:24
【问题描述】:

当我第一次使用另一个画布作为绘图源时,我看到画布绘制速度很慢。在我交换图像之前,后续的画布到画布 .drawImage 调用都很好(然后我再次看到同样的问题)。

下面的示例代码 - 加载了一个图像,然后创建了 4 个画布,第一个画布是从图像本身绘制的,第二个画布是从第一个画布绘制的,等等。创建画布后,源图像被交换并代码再次运行。

        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"

控制台输出是...

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

我的问题是为什么第二张画布绘制缓慢?我尝试了各种图像文件和不同的画布大小,但总是看到相同的结果。我在 Chrome 和 Safari 上测试过。

如果缓慢绘制是在第一个画布上,我可以接受,尽管 .onload 被触发,但图像仍然存在一些问题。但是第二个画布上的速度很慢,即第一个画布是从图像中绘制的,没有问题。

【问题讨论】:

    标签: javascript image canvas drawimage


    【解决方案1】:

    我认为你只是在这里遇到了一个奇怪的优化怪癖,可能很难对发生的事情有一个明确的答案,但无论如何我都会尝试做出有根据的猜测。 p>


    似乎浏览器在 GPU 实际执行分配的作业之前就回到了 CPU 线程,充分利用了任务并行性。

    所以在第一个循环中,GPU 将开始一项工作,要求将&lt;img&gt; 绘制到&lt;canvas&gt;,即使在 Chrome 中解码此图像的数据,仍必须将其传输到 GPU 并转换为一个实际的位图。
    但正如我们所说,这是并行完成的,因此 js 脚本可以继续并直接进行第二个循环同时执行此工作

    但是,当在第二个目标画布上绘制第一个目标画布时,它会看到有一个正在运行的 GPU 作业会修改它,因此会阻塞 CPU 线程,直到第一次绘制完成。

    但下一次迭代将仅处理位图缓冲区已在 GPU 上的 &lt;canvas&gt; 源,因此它们不会花费任何相关时间。

    我们可以通过在每次迭代之间等待几毫秒来以某种方式确认这一点。这样做,所有 canvas-to-canvas 操作将花费大约 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;

    同样,如果我们生成每个来源的 ImageBitmaps,我们可以看到花费最多的时间是预期的&lt;img&gt;

    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:有人可能会想调用 getImageData 来强制同步返回 CPU 线程,但这样做我们也会在 CPU 和 GPU 之间来回传输所有画布位图,实际上在每个循环中都会产生相同的慢速.

    【讨论】:

    • 非常感谢。我没有充分考虑您描述的 CPU / GPU 问题。为了证实-在发布问题之前,我尝试了各种方法来探索“缓慢”的绘制,例如部分绘制图像,绘制到比图像更小的画布,在文档画布中使用,不使用循环(你永远不知道......)等。没有什么影响第一个画布到画布绘制的缓慢性。这符合您对无论如何都必须传输/转换的(总)图像资产的回答。再次感谢您花时间提供出色的答复。
    • 嗯,有趣的答案,但我绝对不同意这是 cpu 与 gpu,这似乎有点过于手摇。上下文是在处理 img 和 canvas 之间切换,而当您在处理的内容之间切换时,问题就会出现。
    猜你喜欢
    • 2015-08-12
    • 2011-04-27
    • 2015-12-03
    • 2012-04-05
    • 1970-01-01
    • 1970-01-01
    • 2011-04-20
    • 1970-01-01
    • 2016-03-24
    相关资源
    最近更新 更多