为什么Chrome在canvas上显示大量图像时会出现问题,而其他浏览器却没有?

13

我们正在使用HTML5画布,在同一时间显示大量图像。

这个工作得很好,但最近我们在Chrome上遇到了一个问题。

当将图像绘制到画布上时,似乎会达到某个点,性能会迅速降低。

这不是缓慢的效果,似乎你从60fps直接降至2-4fps。

这是一些复制代码:

// Helpers
// https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Math/random
function getRandomInt(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; }
// http://www.paulirish.com/2011/requestanimationframe-for-smart-animating/
window.requestAnimFrame = (function () { return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function (callback) { window.setTimeout(callback, 1000 / 60); }; })();
// https://github.com/mrdoob/stats.js
var Stats = function () { var e = Date.now(), t = e; var n = 0, r = Infinity, i = 0; var s = 0, o = Infinity, u = 0; var a = 0, f = 0; var l = document.createElement("div"); l.id = "stats"; l.addEventListener("mousedown", function (e) { e.preventDefault(); y(++f % 2) }, false); l.style.cssText = "width:80px;opacity:0.9;cursor:pointer"; var c = document.createElement("div"); c.id = "fps"; c.style.cssText = "padding:0 0 3px 3px;text-align:left;background-color:#002"; l.appendChild(c); var h = document.createElement("div"); h.id = "fpsText"; h.style.cssText = "color:#0ff;font-family:Helvetica,Arial,sans-serif;font-size:9px;font-weight:bold;line-height:15px"; h.innerHTML = "FPS"; c.appendChild(h); var p = document.createElement("div"); p.id = "fpsGraph"; p.style.cssText = "position:relative;width:74px;height:30px;background-color:#0ff"; c.appendChild(p); while (p.children.length < 74) { var d = document.createElement("span"); d.style.cssText = "width:1px;height:30px;float:left;background-color:#113"; p.appendChild(d) } var v = document.createElement("div"); v.id = "ms"; v.style.cssText = "padding:0 0 3px 3px;text-align:left;background-color:#020;display:none"; l.appendChild(v); var m = document.createElement("div"); m.id = "msText"; m.style.cssText = "color:#0f0;font-family:Helvetica,Arial,sans-serif;font-size:9px;font-weight:bold;line-height:15px"; m.innerHTML = "MS"; v.appendChild(m); var g = document.createElement("div"); g.id = "msGraph"; g.style.cssText = "position:relative;width:74px;height:30px;background-color:#0f0"; v.appendChild(g); while (g.children.length < 74) { var d = document.createElement("span"); d.style.cssText = "width:1px;height:30px;float:left;background-color:#131"; g.appendChild(d) } var y = function (e) { f = e; switch (f) { case 0: c.style.display = "block"; v.style.display = "none"; break; case 1: c.style.display = "none"; v.style.display = "block"; break } }; var b = function (e, t) { var n = e.appendChild(e.firstChild); n.style.height = t + "px" }; return { REVISION: 11, domElement: l, setMode: y, begin: function () { e = Date.now() }, end: function () { var f = Date.now(); n = f - e; r = Math.min(r, n); i = Math.max(i, n); m.textContent = n + " MS (" + r + "-" + i + ")"; b(g, Math.min(30, 30 - n / 200 * 30)); a++; if (f > t + 1e3) { s = Math.round(a * 1e3 / (f - t)); o = Math.min(o, s); u = Math.max(u, s); h.textContent = s + " FPS (" + o + "-" + u + ")"; b(p, Math.min(30, 30 - s / 100 * 30)); t = f; a = 0 } return f }, update: function () { e = this.end() } } }
// Firefox events suck
function getOffsetXY(eventArgs) { return { X: eventArgs.offsetX == undefined ? eventArgs.layerX : eventArgs.offsetX, Y: eventArgs.offsetY == undefined ? eventArgs.layerY : eventArgs.offsetY }; }
function getWheelDelta(eventArgs) { if (!eventArgs) eventArgs = event; var w = eventArgs.wheelDelta; var d = eventArgs.detail; if (d) { if (w) { return w / d / 40 * d > 0 ? 1 : -1; } else { return -d / 3; } } else { return w / 120; }  }

// Reproduction Code
var stats = new Stats();
document.body.appendChild(stats.domElement);

var masterCanvas = document.getElementById('canvas');
var masterContext = masterCanvas.getContext('2d');

var viewOffsetX = 0;
var viewOffsetY = 0;
var viewScaleFactor = 1;
var viewMinScaleFactor = 0.1;
var viewMaxScaleFactor = 10;

var mouseWheelSensitivity = 10; //Fudge Factor
var isMouseDown = false;
var lastMouseCoords = null;

var imageDimensionPixelCount = 25;
var paddingPixelCount = 2;
var canvasDimensionImageCount = 50;
var totalImageCount = Math.pow(canvasDimensionImageCount, 2);

var images = null;

function init() {
    images = createLocalImages(totalImageCount, imageDimensionPixelCount);
    initInteraction();
    renderLoop();
}

function initInteraction() {
    var handleMouseDown = function (eventArgs) {
        isMouseDown = true;
        var offsetXY = getOffsetXY(eventArgs);

        lastMouseCoords = [
            offsetXY.X,
            offsetXY.Y
        ];
    };
    var handleMouseUp = function (eventArgs) {
        isMouseDown = false;
        lastMouseCoords = null;
    }

    var handleMouseMove = function (eventArgs) {
        if (isMouseDown) {
            var offsetXY = getOffsetXY(eventArgs);
            var panX = offsetXY.X - lastMouseCoords[0];
            var panY = offsetXY.Y - lastMouseCoords[1];

            pan(panX, panY);

            lastMouseCoords = [
                offsetXY.X,
                offsetXY.Y
            ];
        }
    };

    var handleMouseWheel = function (eventArgs) {
        var mouseX = eventArgs.pageX - masterCanvas.offsetLeft;
        var mouseY = eventArgs.pageY - masterCanvas.offsetTop;                
        var zoom = 1 + (getWheelDelta(eventArgs) / mouseWheelSensitivity);

        zoomAboutPoint(mouseX, mouseY, zoom);

        if (eventArgs.preventDefault !== undefined) {
            eventArgs.preventDefault();
        } else {
            return false;
        }
    }

    masterCanvas.addEventListener("mousedown", handleMouseDown, false);
    masterCanvas.addEventListener("mouseup", handleMouseUp, false);
    masterCanvas.addEventListener("mousemove", handleMouseMove, false);
    masterCanvas.addEventListener("mousewheel", handleMouseWheel, false);
    masterCanvas.addEventListener("DOMMouseScroll", handleMouseWheel, false);
}

function pan(panX, panY) {
    masterContext.translate(panX / viewScaleFactor, panY / viewScaleFactor);

    viewOffsetX -= panX / viewScaleFactor;
    viewOffsetY -= panY / viewScaleFactor;
}

function zoomAboutPoint(zoomX, zoomY, zoomFactor) {
    var newCanvasScale = viewScaleFactor * zoomFactor;

    if (newCanvasScale < viewMinScaleFactor) {
        zoomFactor = viewMinScaleFactor / viewScaleFactor;
    } else if (newCanvasScale > viewMaxScaleFactor) {
        zoomFactor = viewMaxScaleFactor / viewScaleFactor;
    }

    masterContext.translate(viewOffsetX, viewOffsetY);
    masterContext.scale(zoomFactor, zoomFactor);

    viewOffsetX = ((zoomX / viewScaleFactor) + viewOffsetX) - (zoomX / (viewScaleFactor * zoomFactor));
    viewOffsetY = ((zoomY / viewScaleFactor) + viewOffsetY) - (zoomY / (viewScaleFactor * zoomFactor));
    viewScaleFactor *= zoomFactor;

    masterContext.translate(-viewOffsetX, -viewOffsetY);
}

function renderLoop() {
    clearCanvas();
    renderCanvas();
    stats.update();
    requestAnimFrame(renderLoop);
}

function clearCanvas() {
    masterContext.clearRect(viewOffsetX, viewOffsetY, masterCanvas.width / viewScaleFactor, masterCanvas.height / viewScaleFactor);
}

function renderCanvas() {
    for (var imageY = 0; imageY < canvasDimensionImageCount; imageY++) {
        for (var imageX = 0; imageX < canvasDimensionImageCount; imageX++) {
            var x = imageX * (imageDimensionPixelCount + paddingPixelCount);
            var y = imageY * (imageDimensionPixelCount + paddingPixelCount);

            var imageIndex = (imageY * canvasDimensionImageCount) + imageX;
            var image = images[imageIndex];

            masterContext.drawImage(image, x, y, imageDimensionPixelCount, imageDimensionPixelCount);
        }
    }
}

function createLocalImages(imageCount, imageDimension) {
    var tempCanvas = document.createElement('canvas');
    tempCanvas.width = imageDimension;
    tempCanvas.height = imageDimension;
    var tempContext = tempCanvas.getContext('2d');

    var images = new Array();

    for (var imageIndex = 0; imageIndex < imageCount; imageIndex++) {
        tempContext.clearRect(0, 0, imageDimension, imageDimension);
        tempContext.fillStyle = "rgb(" + getRandomInt(0, 255) + ", " + getRandomInt(0, 255) + ", " + getRandomInt(0, 255) + ")";
        tempContext.fillRect(0, 0, imageDimension, imageDimension);

        var image = new Image();
        image.src = tempCanvas.toDataURL('image/png');

        images.push(image);
    }

    return images;
}

// Get this party started
init();

这是在画布上绘制50 x 50(2500)网格中的50像素x 50像素图像。我还尝试了25像素x 25像素和50 x 50(2500)图像。

我们有其他本地示例,处理更大的图像和更多的图像,而其他浏览器在更高的值时开始出现问题。

作为一个快速测试,我将js fiddle中的代码增加到100像素x 100像素和100 x 100(10000)图像,当完全缩小时仍以16fps运行。(注意:我必须将viewMinScaleFactor降低到0.01才能全部适合缩小时。)

另一方面,Chrome似乎达到某种限制,FPS从60下降到2-4。

以下是您可以交互使用的jsfiddle链接: http://jsfiddle.net/BtyL6/14/


以下是关于我们尝试过的内容和结果信息:
我们尝试使用setinterval而不是requestAnimationFrame。
如果您加载10张图片并将它们每个绘制250次,而不是2500张图片每次绘制一次,则问题会消失。这似乎表明Chrome达到了某种限制/触发器,以存储有关渲染的数据。
在我们更复杂的示例中,我们有剔除(不渲染视觉范围之外的图像),虽然这有所帮助,但它不是一个解决方案,因为我们需要能够同时显示所有图像。
我们只在本地代码发生更改时才呈现图像,这可以帮助(当显然没有更改时),但它不是完整的解决方案,因为画布应该是交互式的。
在示例代码中,我们使用画布创建图像,但是该代码也可以运行,通过命中Web服务提供图像,将看到相同的行为(缓慢)。
我们发现要搜索此问题非常困难,大多数结果都来自几年前,已经过时。如果需要更多信息,请提出!

编辑:更改js fiddle的URL以反映与问题中相同的代码。代码本身实际上并没有改变,只是格式有所改变。但我想保持一致。


编辑:已更新jsfiddle和代码,使用CSS防止选择并在渲染循环完成后调用requestAnim。


我在Chrome 27.0.1453.94和29.0.1516.3 canary中测试了代码。在Chrome canary中,我没有性能问题。即使在低端PC上,所有图像都在屏幕上,我也能获得15 fps的速度。例如,Internet Explorer 10在最小缩放下的性能保持在5 fps。这显然是一个实际的问题,看起来Chrome开发人员已经为下一个官方版本解决了这个问题。 - Gustavo Carvalho
只是想指出Fiddle中的代码存在许多问题。这段代码是活的!有时它可以很好地工作,只需再次点击运行按钮,页面就会冻结,甚至在之前我发布的Canary浏览器中也是如此。肯恩在他的回答中做得很好,顺便说一句。 - Gustavo Carvalho
嗨gfcarv,我的同事在Canary中进行了测试,并发现它改善了一些问题。希望这只是一个回归问题,很快就会得到解决。如上所述,此代码仅是问题的重现。 “真正”的代码具有更多功能,以在某些条件下提高性能。但是,即使像剔除和仅在必要时重新绘制等操作也无法减少最大资源使用量,也无法防止Chrome“触发”此慢速模式。不幸的是,Ken的代码通过不再执行所需的操作(即将图像绘制到画布上)来缓解问题。 - user310988
2个回答

6
在我的电脑上,这段代码在Canary中会冻结。至于为什么Chrome会出现这种情况,简单的答案是它使用了与FF不同的实现方式。我不知道具体细节,但显然在这个领域有优化的空间。
然而,我可以给你一些提示,告诉你如何优化给定的代码,使其在Chrome中也能运行 :-)
这里有几个问题:
- 你将每个颜色块都存储为图像。这似乎对Canary/Chrome有巨大的性能影响。 - 你在循环开始时调用requestAnimationFrame。 - 即使没有变化,你也要清除和渲染。
试着(解决这些问题):
- 如果你只需要纯色块,请直接使用fillRect()进行绘制,并将颜色索引存储在数组中(而不是图像)。即使你将它们绘制到离屏画布上,你也只需要执行一次绘制到主画布上,而不是多次图像绘制操作。 - 将requestAnimationFrame移动到代码块的末尾以避免堆叠。 - 使用脏标记来防止不必要的渲染:
我稍微修改了一下代码,使用纯色块来演示Chrome / Canary中的性能影响。
我在全局范围内设置了一个脏标记为true(用于渲染初始场景),每次鼠标移动时都将其设置为true:
//global
var isDirty = true;

//mouse move handler
var handleMouseMove = function (eventArgs) {

    // other code

    isDirty = true;

    // other code
};

//render loop
function renderLoop() {
    if (isDirty) {
        clearCanvas();
        renderCanvas();
    }
    stats.update();
    requestAnimFrame(renderLoop);
}

//in renderCanvas at the end:
function renderCanvas() {
    // other code
    isDirty = false;
}

当然,您需要在其他地方检查“isDirty”标志的注意事项,并在错误的时刻引入更多条件。我会存储鼠标的旧位置,并且只有在鼠标移动时(如果它发生了变化),才设置脏标志——但是我没有修改这部分内容。
如您所见,您将能够在Chrome和FF中以更高的FPS运行此程序。
我还假设(我没有测试)您可以通过仅绘制填充/间隙而不是清除整个画布来优化“clearCanvas()”函数。但需要进行测试。
添加了一个CSS规则,防止使用鼠标选择画布:
对于像这样的情况,即“事件驱动”,您实际上根本不需要动画循环。只需在坐标或鼠标滚轮更改时调用重绘即可。
修改:
http://jsfiddle.net/BtyL6/10/

嗨Ken,感谢你的努力和详细的回复!颜色块被存储为图像,因为我们正在绘制图像,而不是绘制颜色块,真正的代码使用照片。我已经尝试了移动requestAnim,但没有任何区别,现在我已经改为放在最后,因为MDN和MSDN都显示如此。正如在原帖中提到的那样,真正的代码已经实现了脏重绘和裁剪,这些有助于在某些情况下减少资源使用,但并不减少最大资源使用。再次感谢回复! - user310988
没问题 @AndyJ。您尝试将图像绘制到屏幕外画布进行缓存(“预渲染”),然后使用单个操作从该画布复制到主画布吗? - user1693593
我们本地的一个示例做了这件事,但似乎没有帮助。问题似乎是由Chrome在任何地方绘制一定量的唯一数据到画布中所触发的。我所说的“唯一数据”是指如果您将相同的URL加载到图像中并重复绘制它,甚至是相同的10个URL,则不会出现问题。但是,如果您加载2,500个唯一的URL,则会出现问题。无论URL是外部的还是使用toDataURL创建的,都会发生这种情况。我们有一些示例,可以将其绘制到画布上、绘制到屏幕外画布上、绘制到多个屏幕外画布上。 - user310988
我们有缓动、平移和缩放,所有这些都告诉画布它需要更新,一旦完成,画布就会回到等待再次被告知它是脏的状态。另外:感谢提供防止选择的 CSS 样式,那真的很烦人! - user310988

1

感谢更新。可以确认在Canary 31版本中已经正常工作。 - user1693593

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接