Why JavaScript timing matters

JavaScript often triggers visual changes—sometimes directly through style manipulations, and sometimes through calculations that lead to visual changes, like searching or sorting data. Badly-timed or long-running JavaScript is a common cause of performance issues, so it pays to minimize its impact wherever possible.

Performance profiling JavaScript is something of an art, because the code you write is nothing like the code that actually executes. Modern browsers use JIT compilers and a range of optimizations to deliver the fastest possible execution, which substantially changes the dynamics of your code. Still, there are some concrete steps you can take to help your applications run JavaScript well.

Use requestAnimationFrame for visual updates

When visual changes happen on screen, you want your work to run at the right time for the browser—at the start of the frame. The only way to guarantee that your JavaScript runs at the start of a frame is to use requestAnimationFrame.

/**
    * If run as a requestAnimationFrame callback, this
    * will be run at the start of the frame.
    */
function updateScreen(time) {
    // Make visual updates here.
}

requestAnimationFrame(updateScreen);

Frameworks and samples sometimes use setTimeout or setInterval to drive animations, but the callback then runs at some point in the frame, possibly near the end. That can cause a frame to be missed, resulting in jank.

setTimeout causing the browser to miss a frame.

jQuery used setTimeout for its animate behavior until version 3, when it switched to requestAnimationFrame. If you are on an older jQuery version, patching it to use requestAnimationFrame is strongly advised.

Shorten main-thread work or move it to Web Workers

JavaScript runs on the browser's main thread, alongside style calculations, layout, and often paint. Long-running JavaScript blocks these tasks, and can cause frames to be missed.

Be tactical about when JavaScript runs, and for how long. During movement such as scrolling, keep JavaScript work to roughly 3-4ms—anything longer risks consuming too much frame time. During idle periods, you can afford to be more relaxed.

Pure computational work that does not need DOM access can often move to Web Workers. Data manipulation and traversal—sorting, searching, loading, and model generation—are common fits for this model.

var dataSortWorker = new Worker("sort-worker.js");
dataSortWorker.postMesssage(dataToSort);

// The main thread is now free to continue working on other things...

dataSortWorker.addEventListener('message', function(evt) {
    var sortedData = evt.data;
    // Update data on screen...
});

Not all work fits this model since Web Workers have no DOM access. For work that must live on the main thread, consider batching: segment the larger task into micro-tasks, each no longer than a few milliseconds, and run them inside requestAnimationFrame handlers across frames.

There are UX and UI consequences to this approach. Make sure users know a task is being processed by showing a progress or activity indicator. In any case, this approach keeps the app's main thread free and responsive to user interaction.

Measure your JavaScript's frame cost

When evaluating a framework, library, or your own code, assess how much it costs to run the JavaScript on a frame-by-frame basis. This is especially important for performance-critical animation work, such as transitions and scrolling.

The Performance panel in Chrome DevTools is the best way to measure that cost. It typically produces low-level records like this:

A performance recording in Chrome DevTools

The Main section provides a flame chart of JavaScript calls, letting you analyze exactly which functions were called and how long each took. With that information you can find hotspots and decide whether to remove long-running code or migrate it to a Web Worker, freeing the main thread for other tasks. See the guide on getting started with analyzing runtime performance for details on using the Performance panel.

Don't chase micro-optimizations

Knowing that one approach is 100 times faster than another—such as reading an element's offsetTop versus computing getBoundingClientRect()—is interesting, but functions like these are typically called only a small number of times per frame. Chasing such differences is usually wasted effort that saves fractions of milliseconds.

Games and computationally expensive applications are the exception: they pack a lot of computation into each frame, so every bit helps. For most applications, however, be wary of micro-optimizations because they rarely map to the kind of app you are building.