Debouncing events with requestAnimationFrame() for better performance
Earlier this year, I wrote an article about how scroll
and resize
event listeners can be crippling for performance on certain browsers.
The solution is a technique known as debouncing.
Debouncing is a way of forcing an event listener to wait a certain period of time before firing again.
At the time, I recommended using setTimeout()
with a wait time of 66 milliseconds (the approximate refresh rate of modern monitors) to maximize jank and maximize performance.
// Setup a timer
var timeout;
// Listen for scrolling events
window.addEventListener('scroll', function ( event ) {
console.log( 'no debounce' );
// If timer is null, reset it to 66ms and run your functions.
// Otherwise, wait until timer is cleared
if ( !timeout ) {
timeout = setTimeout(function() {
// Reset timeout
timeout = null;
// Run our scroll functions
console.log( 'debounced' );
}, 66);
}
}, false);
A better approach
There’s a better way to do this, though: requestAnimationFrame()
. Just like setTimeout()
, the requestAnimationFrame()
sets up a callback function. Instead of running after a certain period of time, though, it runs the next time a page paint is requested.
window.requestAnimationFrame(function () {
console.log('paint!');
});
This provides a better way to loop over events, because it runs when the browser paint is actually happening, rather than when we guestimate it will (ever 66 milliseconds).
Here’s the same approach, but with requestAnimationFrame()
instead.
// Setup a timer
var timeout;
// Listen for resize events
window.addEventListener('scroll', function ( event ) {
console.log( 'no debounce' );
// If there's a timer, cancel it
if (timeout) {
window.cancelAnimationFrame(timeout);
}
// Setup the new requestAnimationFrame()
timeout = window.requestAnimationFrame(function () {
// Run our scroll functions
console.log( 'debounced' );
});
}, false);
Browser Compatibility
requestAnimationFrame()
works in all modern browsers, and IE10 and up. You can push support back to older browsers with this polyfill from Paul Irish, which falls back to setTimeout()
.
// http://paulirish.com/2011/requestanimationframe-for-smart-animating/
// http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating
// requestAnimationFrame polyfill by Erik Möller. fixes from Paul Irish and Tino Zijdel
// MIT license
(function() {
var lastTime = 0;
var vendors = ['ms', 'moz', 'webkit', 'o'];
for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame'];
window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame']
|| window[vendors[x]+'CancelRequestAnimationFrame'];
}
if (!window.requestAnimationFrame)
window.requestAnimationFrame = function(callback, element) {
var currTime = new Date().getTime();
var timeToCall = Math.max(0, 16 - (currTime - lastTime));
var id = window.setTimeout(function() { callback(currTime + timeToCall); },
timeToCall);
lastTime = currTime + timeToCall;
return id;
};
if (!window.cancelAnimationFrame)
window.cancelAnimationFrame = function(id) {
clearTimeout(id);
};
}());