Solved

Why does chrome struggle to display lots of images on a canvas when the other browsers don't?

Posted on 2013-06-06
4
216 Views
Last Modified: 2013-11-19
We're working with the HTML5 canvas, displaying lots of images at one time.

This is working pretty well but recently we've had a problem with chrome.

When drawing images on to a canvas you seem to reach a certain point where the performance degrades very quickly.

It's not a slow effect, it seems that you go right from 60fps to 2-4fps.

Here's some reproduction code:

// 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();

Open in new window


And a jsfiddle link for your interactive pleasure: http://jsfiddle.net/BtyL6/14/

This is drawing 50px x 50px images in a 50 x 50 (2500) grid on the canvas. I've also quickly tried with 25px x 25px and 50 x 50 (2500) images.

We have other local examples that deal with bigger images and larger numbers of images and the other browsers start to struggle with these at higher values.

As a quick test I jacked up the code in the js fiddle to 100px x 100px and 100 x 100 (10000) images and that was still running at 16fps when fully zoomed out. (Note: I had to lower the viewMinScaleFactor to 0.01 to fit it all in when zoomed out.)

Chrome on the other hand seems to hit some kind of limit and the FPS drops from 60 to 2-4.

Here's some info about what we've tried and the results:

We've tried using setinterval rather than requestAnimationFrame.

If you load 10 images and draw them 250 times each rather than 2500 images drawn once each then the problem goes away. This seems to indicate that chrome is hitting some kind of limit/trigger as to how much data it's storing about the rendering.

We have culling (not rendering images outside of the visual range) in our more complex examples and while this helps it's not a solution as we need to be able to show all the images at once.

We have the images only being rendered if there have been changes in our local code, against this helps (when nothing changes, obviously) but it isn't a full solution because the canvas should be interactive.

In the example code we're creating the images using a canvas, but the code can also be run hitting a web service to provide the images and the same behaviour (slowness) will be seen.

We've found it very hard to even search for this issue, most results are from a couple of years ago and woefully out of date.

If any more information would be useful then please ask!

NOTE: I am using just blocks of colours here but in the actual application these are images. I am just using colour here to simulate actual images to make testing easier.
0
Comment
Question by:erik_nodland
  • 3
4 Comments
 
LVL 11

Expert Comment

by:mcnute
ID: 39225251
0
 
LVL 4

Author Comment

by:erik_nodland
ID: 39231902
Hi

Thanks for the links. Unfortunately I have seen these posts before and they don't really help my situation. Thanks anyway.

Erik
0
 
LVL 4

Accepted Solution

by:
erik_nodland earned 0 total points
ID: 39301355
Hi

I have opened this as a bug with the Chrome team.

https://code.google.com/p/chromium/issues/detail?id=247912

Thanks
Erik
0
 
LVL 4

Author Closing Comment

by:erik_nodland
ID: 39309811
This looks to be a bug with the Chrome browser and is currently being looked into by the team.
0

Featured Post

Is Your Active Directory as Secure as You Think?

More than 75% of all records are compromised because of the loss or theft of a privileged credential. Experts have been exploring Active Directory infrastructure to identify key threats and establish best practices for keeping data safe. Attend this month’s webinar to learn more.

Question has a verified solution.

If you are experiencing a similar issue, please ask a related question

Suggested Solutions

Use these top 10 tips to master the art of email signature design. Create an email signature design that will easily wow recipients, promote your brand and highlight your professionalism.
Find out what you should include to make the best professional email signature for your organization.
The viewer will learn the basics of jQuery including how to code hide show and toggles. Reference your jQuery libraries: (CODE) Include your new external js/jQuery file: (CODE) Write your first lines of code to setup your site for jQuery…
The viewer will learn how to create a basic form using some HTML5 and PHP for later processing. Set up your basic HTML file. Open your form tag and set the method and action attributes.: (CODE) Set up your first few inputs one for the name and …

895 members asked questions and received personalized solutions in the past 7 days.

Join the community of 500,000 technology professionals and ask your questions.

Join & Ask a Question

Need Help in Real-Time?

Connect with top rated Experts

13 Experts available now in Live!

Get 1:1 Help Now