Canvas 第一次 drawImage 慢另一个 canvas 被用作源参数
Canvas drawImage slow first time another canvas is used as the source argument
第一次使用另一个 canvas 作为绘图源时,我发现 canvas 绘图很慢。随后的 canvas 到 canvas .drawImage 调用没问题,直到我交换图像(然后我再次看到同样的问题)。
下面的示例代码 - 加载一个图像,然后创建 4 个 canvases,第一个 canvas 从图像本身绘制,第二个 canvas 从第 1 等。在创建 canvases 之后,交换源图像并再次使用代码 运行。
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
我的问题是为什么第二个 canvas 绘图很慢?我尝试了各种图像文件和不同的 canvas 大小,但总是看到相同的结果。我已经在 Chrome 和 Safari 上进行了测试。
如果缓慢绘制是在第一个 canvas 我可以接受,尽管 .onload 已触发,但图像仍有一些问题。但是速度慢的是第二个 canvas 即第一个是从图像中毫无问题地绘制出来的。
我认为您刚刚遇到了一个奇怪的优化怪癖,可能很难对发生的事情给出明确的答案,但无论如何我都会尝试做出有根据的猜测。
浏览器似乎在 GPU 实际执行分配给它的作业之前返回到 CPU 线程,充分利用了任务并行性。
因此在第一个循环中,GPU 将开始一项工作,要求将 <img>
绘制到 <canvas>
,该图像的数据即使在 Chrome 中解码,仍然必须传输到 GPU 并转换为实际位图。
但正如我们所说,这是并行完成的,因此 js 脚本可以继续并直接进行第二个循环 同时执行此作业。
然而,当它在第二个目标上绘制第一个目标 canvas 时,它会看到有一个 运行 GPU 作业会修改它,因此会阻止 CPU线程直到第一个绘图完成。
虽然接下来的迭代将只处理 <canvas>
位图缓冲区已经在 GPU 上的源,因此它们不会花费任何相关时间。
我们可以通过在每次迭代之间等待几毫秒来以某种方式确认这一点。这样做,所有 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;
同样,如果我们确实为每个源生成 ImageBitmap,我们可以看到花费最多时间的是预期的 <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:人们可能会想调用 getImageData
以强制同步返回到 CPU 线程,但这样做我们也会传输所有 canvases在 CPU 和 GPU 之间来回位图,在每个循环中实际上产生相同的缓慢。
第一次使用另一个 canvas 作为绘图源时,我发现 canvas 绘图很慢。随后的 canvas 到 canvas .drawImage 调用没问题,直到我交换图像(然后我再次看到同样的问题)。
下面的示例代码 - 加载一个图像,然后创建 4 个 canvases,第一个 canvas 从图像本身绘制,第二个 canvas 从第 1 等。在创建 canvases 之后,交换源图像并再次使用代码 运行。
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
我的问题是为什么第二个 canvas 绘图很慢?我尝试了各种图像文件和不同的 canvas 大小,但总是看到相同的结果。我已经在 Chrome 和 Safari 上进行了测试。
如果缓慢绘制是在第一个 canvas 我可以接受,尽管 .onload 已触发,但图像仍有一些问题。但是速度慢的是第二个 canvas 即第一个是从图像中毫无问题地绘制出来的。
我认为您刚刚遇到了一个奇怪的优化怪癖,可能很难对发生的事情给出明确的答案,但无论如何我都会尝试做出有根据的猜测。
浏览器似乎在 GPU 实际执行分配给它的作业之前返回到 CPU 线程,充分利用了任务并行性。
因此在第一个循环中,GPU 将开始一项工作,要求将 <img>
绘制到 <canvas>
,该图像的数据即使在 Chrome 中解码,仍然必须传输到 GPU 并转换为实际位图。
但正如我们所说,这是并行完成的,因此 js 脚本可以继续并直接进行第二个循环 同时执行此作业。
然而,当它在第二个目标上绘制第一个目标 canvas 时,它会看到有一个 运行 GPU 作业会修改它,因此会阻止 CPU线程直到第一个绘图完成。
虽然接下来的迭代将只处理 <canvas>
位图缓冲区已经在 GPU 上的源,因此它们不会花费任何相关时间。
我们可以通过在每次迭代之间等待几毫秒来以某种方式确认这一点。这样做,所有 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;
同样,如果我们确实为每个源生成 ImageBitmap,我们可以看到花费最多时间的是预期的 <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:人们可能会想调用 getImageData
以强制同步返回到 CPU 线程,但这样做我们也会传输所有 canvases在 CPU 和 GPU 之间来回位图,在每个循环中实际上产生相同的缓慢。