Skip to main content Accessibility Feedback

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

🔥 Hot off the press! I just launched a new pocket guide. Learn about ES6 arrow functions, let and const, function hoisting, and more.

Have any questions or comments about this post? Email me at chris@gomakethings.com or contact me on Twitter at @ChrisFerdinandi.

Get Daily Developer Tips