IE10 laggy rendering for rapid positioning changes - javascript

I am creating a rendering engine for box2djs that uses elements on the page to render rather than canvas, because it is much easier to style and manipulate elements than it is to implement the same effects on Canvas.
Anyways, Chrome (best as always) renders it flawlessly at 60fps the whole time, where IE10 starts lagging once it is dealing with many elements (about 20+ on my machine).
The thing is IE10 beats V8 (Chrome's JS Engine) in the WebKit Sunspider, so I don't understand why it would be more laggy on IE10 than Chrome.
Why does IE10 start lagging when Chrome doesn't if is faster?
My only guess is that IE10 is slower at page rendering and can't handle that many redraws (60 times a second).
Here's my rendering code:
JS
function drawShape(shape) {
if (shape.m_type === b2Shape.e_circleShape) {
var circle = shape,
pos = circle.m_position,
r = circle.m_radius,
ax = circle.m_R.col1,
pos2 = new b2Vec2(pos.x + r * ax.x, pos.y + r * ax.y);
var div = document.getElementById(shape.GetUserData());
if (div != undefined) {
var x = shape.m_position.x - shape.m_radius,
y = shape.m_position.y - shape.m_radius,
r = circle.m_radius;
div.style.left = x + "px";
div.style.top = y + "px";
}
} else {
var poly = shape;
var div = document.getElementById(shape.GetUserData());
if (div != undefined) {
var x = poly.m_position.x - (poly.m_vertices[0].x),
y = poly.m_position.y - (poly.m_vertices[0].y);
div.style.left = x + "px";
div.style.top = y + "px";
}
}
}
If you are unfamiliar with box2d this function is called for each shape from drawWorld() and drawWorld() is called in each tick. I have my ticks set at 1000/60 miliseconds, or 60 frames per second.

My hunch is that IE10 is struggling with the repaint and reflow of your page. So when you render your elements on the page and move them around and what not (with their styling), it'll cause TONS of repaints. As to why it's performing worse than Chrome, it's probably because of the underlying layout/rendering engine.
IE uses the Trident engine, developed by yours truly, Mircosoft, and has been around since IE4.
Chrome on the other hand, uses Webkit, along with Safari and recently, Opera.
Nicole Sullivan has a good article explaining repaint/reflow process: http://www.stubbornella.org/content/2009/03/27/reflows-repaints-css-performance-making-your-javascript-slow/
If you want to improve the performance of your page on IE10, maybe using canvas is your answer.

Related

Canvas.measureText differences on browsers are huge

EDIT: originally I checked only desktop browsers - but with mobile browsers, the picture is even more complicated.
I came across a strange issue with some browsers and its text rendering capabilities and I am not sure if I can do anything to avoid this.
It seems WebKit and (less consistent) Firefox on Android are creating slightly larger text using the 2D Canvas library. I would like to ignore the visual appearance for now, but instead focus on the text measurements, as those can be easily compared.
I have used the two common methods to calculate the text width:
Canvas 2D API and measure text
DOM method
as outlined in this question: Calculate text width with JavaScript
however, both yield to more or less the same result (across all browsers).
function getTextWidth(text, font) {
// if given, use cached canvas for better performance
// else, create new canvas
var canvas = getTextWidth.canvas || (getTextWidth.canvas = document.createElement("canvas"));
var context = canvas.getContext("2d");
context.font = font;
var metrics = context.measureText(text);
return metrics.width;
};
function getTextWidthDOM(text, font) {
var f = font || '12px arial',
o = $('<span>' + text + '</span>')
.css({'font': f, 'float': 'left', 'white-space': 'nowrap'})
.css({'visibility': 'hidden'})
.appendTo($('body')),
w = o.width();
return w;
}
I modified the fiddle a little using Google fonts which allows to perform text measurements for a set of sample fonts (please wait for the webfonts to be loaded first before clicking the measure button):
http://jsfiddle.net/aj7v5e4L/15/
(updated to force font-weight and style)
Running this on various browsers shows the problem I am having (using the string 'S'):
The differences across all desktop browsers are minor - only Safari stands out like that - it is in the range of around 1% and 4% what I've seen, depending on the font. So it is not big - but throws off my calculations.
UPDATE: Tested a few mobile browsers too - and on iOS all are on the same level as Safari (using WebKit under the hood, so no suprise) - and Firefox on Android is very on and off.
I've read that subpixel accuracy isn't really supported across all browsers (older IE's for example) - but even rounding doesn't help - as I then can end up having different width.
Using no webfont but just the standard font the context comes with returns the exact same measurements between Chrome and Safari - so I think it is related to webfonts only.
I am a bit puzzled of what I might be able to do now - as I think I just do something wrong as I haven't found anything on the net around this - but the fiddle is as simple as it can get. I have spent the entire day on this really - so you guys are my only hope now.
I have a few ugly workarounds in my head (e.g. rendering the text on affected browsers 4% smaller) - which I would really like to avoid.
It seems that Safari (and a few others) does support getting at sub-pixel level, but not drawing...
When you set your font-size to 9.5pt, this value gets converted to 12.6666...px.
Even though Safari does return an high precision value for this:
console.log(getComputedStyle(document.body)['font-size']);
// on Safari returns 12.666666984558105px oO
body{font-size:9.5pt}
it is unable to correctly draw at non-integer font-sizes, and not only on a canvas:
console.log(getRangeWidth("S", '12.3px serif'));
// safari: 6.673828125 | FF 6.8333282470703125
console.log(getRangeWidth("S", '12.4px serif'));
// safari: 6.673828125 | FF 6.883331298828125
console.log(getRangeWidth("S", '12.5px serif'));
// safari 7.22998046875 | FF 6.95001220703125
console.log(getRangeWidth("S", '12.6px serif'));
// safari 7.22998046875 | FF 7
// High precision DOM based measurement
function getRangeWidth(text, font) {
var f = font || '12px arial',
o = $('<span>' + text + '</span>')
.css({'font': f, 'white-space': 'nowrap'})
.appendTo($('body')),
r = document.createRange();
r.selectNode(o[0]);
var w = r.getBoundingClientRect().width;
o.remove();
return w;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
So in order to avoid these quirks,
Try to always use px unit with integer values.
I found below solution from MDN more helpful for scenarios where fonts are slanted/italic which was for me the case with some google fonts
copying the snippet from here - https://developer.mozilla.org/en-US/docs/Web/API/TextMetrics#Measuring_text_width
const computetextWidth = (text, font) => {
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
context.font = font;
const { actualBoundingBoxLeft, actualBoundingBoxRight } = context.measureText(text);
return Math.ceil(Math.abs(actualBoundingBoxLeft) + Math.abs(actualBoundingBoxRight));
}

Faster way of scaling text in browser? (help interpret test)

I need to scale lots of text nodes in the browser (support of all modern desktop and mobile browsers).
If I am right there are two options that offer good performance: scaling text objects in Canvas or scaling text nodes in the DOM using transform:matrix.
I have created a scenario to test both versions but the results are inconclusive. Uncomment testDOM() or testCanvas() function to start the test. (I am using JQuery and CreateJS framework because it was convenient. It is possible to use vanilla JS but I don't think that is the bottleneck here). (It matters what portion of the screen you actually see so please switch to full screen view in codepen).
http://codepen.io/dandare/pen/pEJyYG
var WIDTH = 500;
var HEIGHT = 500;
var COUNT = 200;
var STEP = 1.02;
var MIN = 0.1;
var MAX = 10;
var stage;
var canvas;
var bg;
var canvasTexts = [];
var domTexts = [];
var domMatrix = [];
var dom;
function testDOM() {
for (var i = 0; i < COUNT; i++) {
var text = $("<div>Hello World</div>");
var scale = MIN + Math.random() * 10;
var matrix = [scale, 0, 0, scale, Math.random() * WIDTH, Math.random() * HEIGHT];
text.css("transform", "matrix(" + matrix.join(',') + ")");
domTexts.push(text);
domMatrix.push(matrix);
}
dom = $('#dom');
dom.append(domTexts);
setTimeout(tickDOM, 1000);
}
function tickDOM() {
for (var i = 0; i < domTexts.length; i++) {
var text = domTexts[i];
var matrix = domMatrix[i];
var scale = matrix[0];
scale *= STEP;
if (scale > MAX)
scale = MIN;
matrix[0] = matrix[3] = scale;
text.css("transform", "matrix(" + matrix.join(',') + ")");
}
requestAnimationFrame(tickDOM);
}
function testCanvas() {
$('#dom').hide();
stage = new createjs.Stage('canvas');
createjs.Touch.enable(stage);
createjs.Ticker.timingMode = createjs.Ticker.RAF;
canvas = stage.canvas;
devicePixelRatio = window.devicePixelRatio || 1;
stage.scaleX = devicePixelRatio;
stage.scaleY = devicePixelRatio;
console.log('devicePixelRatio = ' + devicePixelRatio);
stage.mouseMoveOutside = true;
stage.preventSelection = false;
stage.tickEnabled = false;
stage.addChild(bg = new createjs.Shape());
bg.graphics.clear();
bg.graphics.f('#F2F2F2').drawRect(0, 0, 2 * WIDTH, HEIGHT);
canvas.width = 2 * WIDTH * devicePixelRatio;
canvas.height = HEIGHT * devicePixelRatio;
canvas.style.width = 2 * WIDTH + 'px';
canvas.style.height = HEIGHT + 'px';
stage.update();
for (var i = 0; i < COUNT; i++) {
var text = new createjs.Text("Hello World", "10px", "#333333");
text.scaleX = text.scaleY = MIN + Math.random() * 10;
text.x = Math.random() * WIDTH;
text.y = Math.random() * HEIGHT;
stage.addChild(text);
canvasTexts.push(text);
}
stage.update();
setTimeout(tickCanvas, 1000);
}
function tickCanvas() {
for (var i = 0; i < canvasTexts.length; i++) {
var text = canvasTexts[i];
text.scaleX = text.scaleY *= STEP;
if (text.scaleX > MAX)
text.scaleX = text.scaleY = MIN;
}
stage.update();
requestAnimationFrame(tickCanvas);
}
testDOM();
//testCanvas();
My questions:
Is it possible to improve the performance of my tests? Am I doing something wrong?
The first 5-10 seconds are significantly slower but I don't understand why. Does the browser somehow cashes the text objects after some time? If yes, is the test unusable for real world scenario testing where the objects don't zoom in a loop for longer period of time?
According to the Chrome Profiling tool the DOM version leaves 40% more idle time (is 40% more faster) than the Canvas version but the Canvas animation looks much smoother (after the initial 5-10 seconds of lagging), how should I interpret the Profiling tool results?
In the DOM version I am trying to hide the parent of the text nodes before I apply the transformations and then unhide it but it probably does not matter because transform:matrix on absolutely positioned element does not cause reflow, am I right?
The DOM text nodes have some advantages over the Canvas nodes like native mouse over detection with cursor: pointer or support for decorations (you can not have underlined text in Canvas). Anything else I should know?
When setting the transform:matrix I have to create a string that the compiler must to parse back to numbers, is there a more efficient way of using transform:matrix?
Q.1
Is it possible to improve the performance of my tests? Am I doing
something wrong?
Yes and no. (yes improve and no nothing inherently wrong (ignoring jQuery))
Performance is browser, and device dependent, for example Firefox handles objects better than arrays, while Chrome prefers arrays. There is a long list of differences just for the javascript.
Then the rendering is a dependent on the hardware, How much memory, what capabilities, and the particular drivers. Some hardware hates state changes, while others handle them at full speed. Limiting state changes can improve the speed on one machine while the extra code complexity will impact devices that don't need the optimisation.
The OS also plays a part.
Q.2
The first 5-10 seconds are significantly slower but I don't understand
why. Does the browser somehow cashes the text objects after some time?
If yes, is the test unusable for real world scenario testing where the
objects don't zoom in a loop for longer period of time?
Performance testing in Javascript is very complicated and as a whole application (like your test) is not at all practical.
Why slow?
Many reasons, moving memory to the display device, javascript optimising compilers that run while the codes runs and will recompile if it sees fit, this impacts the performance Un-optimised JS is SLOOOOOWWWWWWWW... and you are seeing it run unoptimised.
As well. In an environment like code pen you are also having to deal with all its code that runs in the same context as yours, it has memory, dom, cpu, GC demands in the same environment as yours and thus your code can not be said to be isolated and profiling results accurate.
Q.3
According to the Chrome Profiling tool the DOM version leaves 40% more
idle time (is 40% more faster) than the Canvas version but the Canvas
animation looks much smoother (after the initial 5-10 seconds of
lagging), how should I interpret the Profiling tool results?
That is the nature of requestAnimationFrame (rAF), it will wait till the next frame is ready before it calls your function. Thus if you run 1ms past 1/60th of a second you have missed the presentation of the current display refresh and rAF will wait till the next is due 1/60th minus 1ms before presentation and the next request is called. This will result in ~50% idle time.
There is not much that can be done than make you render function smaller and call it more often, but then you will get extra overhead with the calls.
rAF can be called many times during a frame and will present all renders during that frame at the same time. That way you will not get the overrun idle time if you keep an eye on the current time and ensure you do not overrun the 1/60th second window of opportunity.
Q.4
In the DOM version I am trying to hide the parent of the text nodes
before I apply the transformations and then unhide it but it probably
does not matter because transform:matrix on absolutely positioned
element does not cause reflow, am I right?
Reflow will not be triggered until you exit the function, hiding the parent at the start of a function and then unhiding it at the end will not make much difference. Javascript is blocking, that means nothing will happen while you are in a function.
Q.5
The DOM text nodes have some advantages over the Canvas nodes like
native mouse over detection with cursor: pointer or support for
decorations (you can not have underlined text in Canvas). Anything
else I should know?
That will depend on what the intended use is. DOM offers a full API for UI and presentation. Canvas offers rendering and pixel manipulation. The logic I use is if it takes more code to do it via DOM then canvas, then it is a canvas job and visa versa
Q.6
When setting the transform:matrix I have to create a string that the
compiler must to parse back to numbers, is there a more efficient way
of using transform:matrix?
No. That is the CSS way.

Improving collision detection performance in HTML5/Javascript

I'm currently creating a game that involves coins that come up from the bottom of the screen in a random X position. If the hero overlaps/collides with the coins, the user gets a point.
My issue is, when I put my collision detection in a fast interval function, it seems that the collision is not registering at some instances.
I've replicated the game in the jsfiddle below (originally it uses the phone's accelerometer to move the hero but for this example I've quickly made it a left/right key based control) -
http://jsfiddle.net/wpavxn6k/1/
Snippet of the interval below -
//keep checking to see if collision has occured
window.setInterval(function () {
if (hitTest(app.hero, app.coinClassUnique) == true) {
app.coinClassUnique.css('display', 'none');
app.score++;
$("h2").html(app.score);
}
}, 5);
//measure the width/height and position of the coin div and the hero image to determine if they are overlapping
function hitTest(a, b) {
var aPos = a.offset();
var bPos = b.offset();
var aLeft = aPos.left;
var aRight = aPos.left + a.width();
var aTop = aPos.top;
var aBottom = aPos.top + a.height();
var bLeft = bPos.left;
var bRight = bPos.left + b.width();
var bTop = bPos.top;
var bBottom = bPos.top + b.height();
return !(bLeft > aRight || bRight < aLeft || bTop > aBottom || bBottom < aTop);
}
My question is whether you can see an obvious flaw in my code that may be affecting performance, or if there are any tips of how I can improve performance and collision detection in a real-time based game like this.
I just wanted to post the answer for this question incase someone encounters a similar issue. The interval of the game was happening at different times so I rebuilt the game inside a Canvas element with requestFrameAnimation. This way, all the elements reload at once.
I had some issues like images not loading properly etc. at first, which is why I avoided using canvas - but with the help of preloading and rendering the images into separate canvases that are hidden off screen - I was able to tackle these issues.

Canvas performance change in Chrome

I'm working on an animation library, and every once in a while I run a benchmark test to see how much of a gain or loss I get with certain features. Recently I've run into something that has me quite perplexed, perhaps someone with more knowledge can shine a light on this for me.
Performance Before:
Chrome: ~4460 sprites # 30fps
Safari: ~2817 sprites # 30fps
FireFox: ~1273 sprites # 30fps
iPhone 4S: ~450 # 30fps
Peformance Now:
Chrome: ~3000 sprites # 30fps
Safari: ~2950 sprites # 30fps
FireFox: ~1900 sprites # 30fps (before Garbage Collection becomes too distracting)
iPhone 4S: ~635 # 30fps
So you can see, Chrome took quite a hit in performance, while every other browser seems to have gotten a little better over this time frame. The biggest thing I notice, and what I'm figuring is the answer, is that the CPU usage seems to have been throttled back in Chrome (I swear before I could get up near 90%, now its maxing around 60%). The majority of the CPU is being used for the drawImage() call, and I'm not sure I can do anything to optimize that.
If its simply an issue where Chrome is now limiting my CPU usage, I'm fine with that.
Any insight would be greatly appreciated...
_s.Sprite.prototype.drawBasic = function() {
var s = this.ctx;
if(s.globalAlpha!=this._alpha) s.globalAlpha = this._alpha;
var width = this.width;
var height = this.height;
var x = this._x;
var y = this._y;
if (_s.snapToPixel) {
x = this._x + (this._x < 0 ? -1 : 0) | 0;
y = this._y + (this._y < 0 ? -1 : 0) | 0;
height = height + (height < 0 ? -1 : 0) | 0;
height = height + (height < 0 ? -1 : 0) | 0;
}
var frame = this.sequence[this.frame] || 0;
var sheetY = frame + (frame < 0 ? -1 : 0) | 0;
var sheetX = (frame - sheetY) * this.spriteSheetX || 0;
s.drawImage(this.bitmap.image, this.bitmap.frameRect.x2 * sheetX, this.bitmap.frameRect.y2 * sheetY, this.bitmap.frameRect.x2, this.bitmap.frameRect.y2, x - (width * this._scaleX) * this.anchorX, y - (height * this._scaleX) * this.anchorY, width * this._scaleX, height * this._scaleY);
this.updateFrame();
};
UPDATE
So I downloaded an old version of Chrome (25.0.1364.5), and ran my benchmark test:
Then I reran in the most current version of Chrome:
Clearly Chrome has changed. Was it on purpose? I don't know. You can see that in the old version of Chrome I've actually gained more performance over my original 4460 (+ ~400, my optimizations must have worked), but you can also see it lets me hover at 100% cpu usage. 2x cpu almost 2x object on the screen.
Update
setInterval doesn't have the issue. Only happens with requestAnimationFrame. This finally makes so much sense. requestAnimationFrame already throttles things to 60fps, what I wasn't aware of, and can't seem to find any info on is that Chrome (others?) throttle it to 30 (60/2) and then 20 (60/3) and probably 15(60/4)... this keeps it in sync with 60hz, so you never end up with 40fps that looks strange because its out of sync with your screen refresh rate.
This explains a lot. I'm really enjoying the cpu savings this provides us.
Updated
An example without any of my code... http://www.goodboydigital.com/pixijs/canvas/bunnymark/ if you run this in Chrome... you will see the point when it jumps from ~60fps straight to 30fps. You can keep adding more bunnies, pixy can handle it... Chrome is throttling the fps. This is not how Chrome use to behave.
So I figured out whats going on here. It's not that performance has changed per say, I can still get 4800 objects on the screen at 30fps. What has changed seems to be the way Chrome tries to optimize the end users experience. It actually throttles things down from 60fps to ~30fps (29.9fps according to dev tools), which causes if(fps>=30) to return false:
stage.onEnterFrame=function(fps){ // fps = the current system fps
if(fps>=30){ // add astroids until we are less than 30fps
stage.addChild(new Asteroid());
}
}
For some reason around 2800 objects, Chrome throttles down to 30fps instead of trying to go as fast as possible... So if I start the benchmark with 4800 objects, it stays at a wonderfully consistent 29.9fps.
(you can see here that its either 60fps or 29.9fps no real in-between, the only thing that changes is how often it switches)
This is the code used for stage timing...
_s.Stage.prototype.updateFPS = function() {
var then = this.ctx.then;
var now = this.ctx.now = Date.now();
var delta = now - then;
this.ctx.then = now;
this.ctx.frameRatio = 60 / (1000 / delta);
};
Hopefully this helps someone else down the road.

canvasContext.fillRect throws NS_ERROR_FAILURE exception in Firefox

I'm trying to draw a huge canvas rectangle on top of the page (some kind of lightbox background), the code is quite straightforward:
var el = document.createElement('canvas');
el.style.position = 'absolute';
el.style.top = 0;
el.style.left = 0;
el.style.zIndex = 1000;
el.width = window.innerWidth + window.scrollMaxX;
el.height = window.innerHeight + window.scrollMaxY;
...
document.body.appendChild(el);
// and later
var ctx = el.getContext("2d");
ctx.fillStyle = "rgba(0, 0, 0, 0.4)";
ctx.fillRect(0, 0, el.width, el.height);
And sometimes (not always) the last line throws:
Component returned failure code: 0x80004005 (NS_ERROR_FAILURE) [nsIDOMCanvasRenderingContext2D.fillRect]
I've been guessing if that happens because of image size or because of the content type beneath the canvas (e.g. embeded video playing), but apparently not.
So I'm looking for any ideas on how to isolate and/or solve this issue.
Looking at the nsIDOMCanvasRenderingContext2D.fillRect() implementation (and going through the functions it calls) - there aren't all too many conditions that will return NS_ERROR_FAILURE. It can only happen if either EnsureSurface() or mThebes->CopyPath() fail. And the following two lines in EnsureSurface() are most likely the source of your issue:
// Check that the dimensions are sane
if (gfxASurface::CheckSurfaceSize(gfxIntSize(mWidth, mHeight), 0xffff)) {
What's being checked here:
Neither the width nor the height of the canvas can exceed 65535 pixels.
The height cannot exceed 32767 pixels on Mac OS X (platform limitation).
The size of canvas data (width * height * 4) cannot exceed 2 GB.
If any of these conditions is violated EnsureSurface() will return false and consequently produce the exception you've seen. Note that the above are implementation details that can change at any time, you shouldn't rely on them. But they might give you an idea which particular limit your code violates.
You could apply a try-catch logic. Firefox seems to be the only browser which behaves this a bit odd way.
el.width = window.innerWidth + window.scrollMaxX;
el.height = window.innerHeight + window.scrollMaxY;
// try first to draw something to check that the size is ok
try
{
var ctx = el.getContext("2d");
ctx.fillRect(0, 0, 1, 1);
}
// if it fails, decrease the canvas size
catch(err)
{
el.width = el.width - 1000;
el.height = el.height - 1000;
}
I haven't found any variable that tells what is the maximum canvas size. It varies from browser to browser and device to device.
The only cross browser method to detect the maximum canvas size seems to be a loop that decreases the canvas size eg. by 100px until it doesn't produce the error. I tested a loop, and it is rather fast way to detect the maximum canvas size. Because other browsers doesn't throw an error when trying to draw on an over sized canvas, it is better to try to draw eg. red rect and read a pixel and check if it is red.
To maximize detection performance:
- While looping, the canvas should be out of the DOM to maximize speed
- Set the starting size to a well known maximum which seems to be 32,767px (SO: Maximum size of a <canvas> element)
- You can make a more intelligent loop which forks the maximum size: something like using first bigger decrease step (eg.1000px) and when an accepted size is reached, tries to increase the size by 500px. If this is accepted size, then increase by 250px and so on. This way the maximum should be found in least amount of trials.

Categories