Почему хром пытается отображать множество изображений на холсте, когда другие браузеры этого не делают?

Мы работаем с холстом HTML5, одновременно отображая множество изображений.

Это работает очень хорошо, но в последнее время у нас была проблема с хром.

При рисовании изображений на холсте вы, похоже, достигаете определенной точки, где производительность очень быстро ухудшается.

Это не медленный эффект, кажется, что вы идете с 60 кадров в секунду до 2-4 кадров в секунду.

Здесь приведен код воспроизведения:

// 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/

Это рисунок 50px x 50px в сетке размером 50 x 50 (2500) на холсте. Я также быстро попробовал с изображениями 25px x 25px и 50 x 50 (2500).

У нас есть другие локальные примеры, которые имеют дело с большими изображениями и большим количеством изображений, а другой браузер начинает бороться с ними при более высоких значениях.

В качестве быстрого теста я поднял код в скрипте js до 100px x 100px и 100 x 100 (10000) изображений, и он все еще работал со скоростью 16 кадров в секунду при полном уменьшении. (Примечание: мне нужно было понизить viewMinScaleFactor до 0,01, чтобы вместить его все при увеличении.)

Хром, с другой стороны, похоже, имеет какой-то предел, а FPS падает от 60 до 2-4.


Вот некоторая информация о том, что мы пробовали и результаты:

Мы попытались использовать setinterval, а не requestAnimationFrame.

Если вы загружаете 10 изображений и рисуете их по 250 раз, а не 2500 изображений, нарисованных один раз, то проблема исчезает. Это, по-видимому, указывает на то, что хром сталкивается с каким-то ограничением/триггером относительно того, сколько данных он хранит в отношении рендеринга.

В наших более сложных примерах мы отбраковываем (а не визуализируем изображения за пределами визуального диапазона), а это не помогает, поскольку мы должны иметь возможность отображать все изображения сразу.

У нас есть изображения, которые отображаются только в том случае, если в нашем локальном коде были изменения. Это помогает (когда ничего не меняется, очевидно), но это не полное решение, потому что холст должен быть интерактивным.

В примере кода мы создаем изображения с помощью холста, но код также можно запустить, ударив веб-службу, чтобы обеспечить изображения, и будет видно то же поведение (медленность).


Нам было очень трудно даже поискать эту проблему, большинство результатов - пару лет назад и ужасно устаревшие.

Если вам нужна дополнительная информация, пожалуйста, спросите!


EDIT: Изменен URL-адрес js-скрипта, чтобы отобразить тот же код, что и в вопросе. Сам код на самом деле не изменился, просто форматирование. Но я хочу быть последовательным.


EDIT: Обновлен jsfiddle и код css для предотвращения выбора и вызова requestAnim после выполнения цикла рендеринга.

Ответ 2

В Canary этот код замораживает его на моем компьютере. Что касается того, почему это происходит в Chrome, простой ответ заключается в том, что он использует другую реализацию, чем f.ex. FF. Подробные сведения я не знаю, но есть определенная возможность для оптимизации реализации в этой области.

Я могу дать несколько советов о том, как вы можете оптимизировать данный код, чтобы он работал в Chrome: -)

Здесь есть несколько вещей:

  • Вы сохраняете каждый блок цветов в виде изображений. Это, похоже, оказывает огромное влияние на Canary/Chrome.
  • Вы вызываете requestAnimationFrame в начале цикла
  • Вы очищаете и выполняете рендеринг, даже если нет изменений

Попробуйте (обращаясь к точкам):

  • Если вам нужны только сплошные блоки цветов, нарисуйте их непосредственно с помощью fillRect() и сохраните индексы цвета в массиве (вместо изображений). Даже если вы нарисуете их на экранном холсте, вам нужно будет сделать одну нитку на основном холсте вместо нескольких операций рисования изображений.
  • Переместите requestAnimationFrame в конец блока кода, чтобы избежать укладки.
  • Используйте грязный флаг для предотвращения ненужного рендеринга:

Я немного изменил код - я изменил его, чтобы использовать сплошные цвета, чтобы продемонстрировать, где влияние производительности в Chrome/Canary.

Я устанавливаю грязный флаг в глобальной области действия как истинный (для рендеринга исходной сцены), который устанавливается в 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/