我们正在使用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();
为您的互动乐趣提供一个jsfiddle链接: http://jsfiddle.net/BtyL6/14/
这是在画布上以50 x 50(2500)网格绘制50px x 50px图像。我也很快尝试了25px x 25px和50 x 50(2500)的图像。
我们还有其他本地示例处理更大的图像和更多的图像,而另一个浏览器开始在更高的值下与这些图像斗争。
作为一个快速测试,我将js中的代码加载到100px x 100px和100 x 100(10000)图像中,当完全缩小时仍然以16fps运行。 (注意:我必须将viewMinScaleFactor降低到0.01,以便在缩小时将其全部放入。)
另一方面,Chrome似乎达到某种限制,FPS从60降至2-4。以下是有关我们尝试过的内容和结果的一些信息:
我们尝试过使用setinterval而不是requestAnimationFrame。
如果您加载10张图像并每次绘制250次而不是每次绘制2500张图像,则问题就会消失。这似乎表明chrome正在针对渲染存储的数据量达到某种限制/触发条件。
我们在更复杂的示例中进行了剔除(不在可视范围之外渲染图像),虽然这有助于它不是解决方案,因为我们需要能够一次显示所有图像。
如果我们的本地代码发生了变化,我们只会渲染图像,这有助于(当没有任何变化时),但它不是一个完整的解决方案,因为画布应该是交互式的。
在示例代码中,我们使用画布创建图像,但也可以运行代码来运行Web服务以提供图像,并且可以看到相同的行为(缓慢)。
我们发现甚至很难找到这个问题,大多数结果来自几年前,并且可悲地过时了。
如果有更多信息有用,请询问!
编辑:更改了js小提琴网址,以反映与问题中相同的代码。代码本身并没有实际改变,只是格式化。但我想保持一致。
编辑:使用css更新了jsfiddle和代码以防止选择并在渲染循环完成后调用requestAnim。
答案 0 :(得分:6)
在Canary中,此代码会将其冻结在我的计算机上。至于为什么这种情况发生在Chrome中,简单的答案是它使用了与f.ex不同的实现。 FF。我不知道深入细节,但显然有优势在这方面优化实施。
我可以提供一些提示,但你如何优化给定的代码以使其在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规则,以防止在使用鼠标时选择画布:
为了在诸如事件驱动之类的情况下进一步优化,您实际上根本不需要动画循环。当坐标或鼠标滚轮发生变化时,您可以调用重绘。
答案 1 :(得分:1)
这是Chrome中的合法错误。
https://code.google.com/p/chromium/issues/detail?id=247912
它现在已经修复,很快就会在chrome主线上发布。