Recently I learned about:
https://bugs.chromium.org/p/chromium/issues/detail?id=570845#c9
Chrome 47 has a new feature which lowers the priority of timers during active input when the timers appear to be SO expensive that we can’t run them while handling input at 60fps ( issue 463143 , Scheduling JS Timer Execution - Google Docs). When the user is actively manipulating something, you should be responding to their input in <16ms in order to ensure a smooth experience for the user.
…
If you need a more guaranteed way of doing work on every frame you should be using requestAnimationFrame (but still aiming to get all your work done <16ms).
This information resolved 2 giant mysteries we had for the last year at Discourse.
Mystery #1
Sometimes when we hit back and scroll down the topic list, Chrome appears to add a 1-3 second delay loading more topics
Mystery #2
When scrolling through topics our widget on the right often did some REAL weird things on slower systems, like this:
I managed to debug and reproduce this on local by using the ember production bundle and setting Chrome to 6x slowdown. I quickly became apparent Chrome does some serious punishing on setTimeout if you start taking too long with it.
In particular I am able to measure 1-3 second delays on timeouts that are meant to take only 10 milliseconds.
This discover is not new @runspired and @rwjblue have known about this issue for a while per:
requestAnimationFrame is basically a much better setTimeout with a few useful additional behaviors. It’s ties to the platform’s refresh rate and will execute the given work during the next refresh. Additionally, any work scheduled into it becomes non-DOM affecting until all scheduled work completes.
Now I am not totally comfortable with the implementation there for Discourse for a couple of reasons:
-
For all we know in some cases ids can overlap and the above code can incorrectly clear the wrong thing.
-
For Discourse we need to be more aggressive and it seems that to get this going right I need to just treat all timeouts under say 16 or so as
requestAnimationFrame
which in turn gives us a simple requestAnimationFrame debounce. Otherwise we are going to need to walk our internal code and use debounce 0 which is not really semantically what we mean, debounce 10 is a lot closer to the intention. I guess what we really want is to have an internaldebounceAnimationFrame
but its a bit of a bigger change.
My proposed implementation for Discourse is:
Ember.run.backburner._platform.setTimeout = function(method, wait) {
// 16 is one animation frame approximately, so bypass
if (!wait || wait < 16 || !window.requestAnimationFrame) {
return [null, window.requestAnimationFrame(method, wait)];
} else {
return [setTimeout(method, wait)];
}
}
Ember.run.backburner._platform.clearTimeout = function(id) {
if (id[0] === null) {
window.cancelAnimationFrame(id[1]);
} else {
clearTimeout(id[0]);
}
}
This seems to work, but I wanted some advice from the Ember team about this, what are your thoughts? Should we go with this or something else? Does any of this belong in Ember?