Frontend Development 16 min read

How to Achieve 60fps Web Animations with requestAnimationFrame

Learn the essential techniques for creating silky‑smooth, 60 fps web animations by understanding frame timing, reducing layout and paint costs, leveraging requestAnimationFrame, applying hardware acceleration, and optimizing rendering pipelines to avoid jank and improve performance across browsers.

QQ Music Frontend Team
QQ Music Frontend Team
QQ Music Frontend Team
How to Achieve 60fps Web Animations with requestAnimationFrame

Preface

High‑performance web animation is a well‑known topic, but there are still many practical, newer techniques worth sharing. After reading this article you will understand the rendering mechanism of animations and the key factors for achieving 60 fps, enabling you to solve animation problems at their source.

Main Content

What is high‑performance animation?

Animation quality is often measured by frame rate; a smooth experience usually requires 60 fps.

Each frame must be rendered within 16.7 ms (1000 ms / 60). Reducing unnecessary work is the primary goal; otherwise frames will be dropped, and you may need to fall back to 30 fps.

How to achieve silky‑smooth animation

The two decisive factors are:

Frame Timing – when a new frame is ready

Frame Budget – how long it takes to render the frame

When to start drawing

Developers often use

setTimeout(callback, 1/60)

to schedule the next frame, but

setTimeout

is inaccurate because it depends on the browser’s internal clock. For example, older IE versions fire at 15.6 ms intervals, so a 16.7 ms timeout may actually wait two intervals, adding ~14.5 ms of delay.

Furthermore,

setTimeout

callbacks are placed in an asynchronous queue; if a synchronous script runs before the timer fires, it will execute first, causing additional delay.

<code>function runForSeconds(s) {
  var start = +new Date();
  while (start + s * 1000 > (+new Date())) {}
}

document.body.addEventListener("click", function () {
    runForSeconds(10);
}, false);

setTimeout(function () {
    console.log("Done!");
}, 1000 * 3);
</code>

Because of these issues, we recommend using

requestAnimationFrame(callback)

instead.

window.requestAnimationFrame() tells the browser you want to perform an animation and requests that the browser calls the specified function before the next repaint. – MDN

When you call this function you ask the browser to:

Provide a new frame.

Execute the supplied callback when that frame is rendered.

Compared with

setTimeout

, rAF lets the system decide the callback timing. If the display refresh rate is 60 Hz, the callback runs every 16.7 ms; at 75 Hz it runs every 13.3 ms. This throttles the function to one call per repaint, preventing dropped frames and stutter.

rAF also automatically lowers the frequency to ~30 fps when the callback cannot finish within a frame, which is still smoother than dropping frames.

When the page is hidden or minimized,

setTimeout

continues to run in the background, wasting CPU cycles. rAF pauses when the page is not visible, resuming when it becomes active again, saving resources.

A simple polyfill for older browsers:

<code>window.requestAnimFrame = (function(){
  return  window.requestAnimationFrame   ||
          window.webkitRequestAnimationFrame ||
          window.mozRequestAnimationFrame    ||
          window.oRequestAnimationFrame      ||
          window.msRequestAnimationFrame     ||
          function(callback){ window.setTimeout(callback, 1000 / 60); };
})();
</code>

Time to draw a frame

rAF solves the timing problem but not the cost of drawing. To improve performance you must optimise the browser’s rendering pipeline.

Rendering

When a page first loads, the browser downloads and parses HTML into a DOM content tree, and parses CSS into a render tree. The rendering engine builds these structures separately for performance.

The most time‑consuming step at this stage is Layout.

<code>function update(timestamp) {
  for (var m = 0; m < movers.length; m++) {
    // DEMO version (slow)
    // movers[m].style.left = ((Math.sin(movers[m].offsetTop + timestamp/1000)+1) * 500) + 'px';
    // FIXED version (fast)
    movers[m].style.left = ((Math.sin(m + timestamp/1000)+1) * 500) + 'px';
  }
  rAF(update);
};
rAF(update);
</code>

The demo version is slow because reading

offsetTop

forces a layout (reflow) on every iteration.

Frequent read/write of layout‑affecting properties causes "layout thrashing". Example:

<code>var h1 = element1.clientHeight;
element1.style.height = (h1 * 2) + 'px';
var h2 = element2.clientHeight;
element2.style.height = (h2 * 2) + 'px';
var h3 = element3.clientHeight;
element3.style.height = (h3 * 2) + 'px';
</code>

Solution: read all needed values first, then write:

<code>// Read
var h1 = element1.clientHeight;
var h2 = element2.clientHeight;
var h3 = element3.clientHeight;
// Write
element1.style.height = (h1 * 2) + 'px';
element2.style.height = (h2 * 2) + 'px';
element3.style.height = (h3 * 2) + 'px';
</code>

Libraries such as

fastdom.js

automate this pattern, and you can also defer all writes to the next frame using rAF.

Paint

After layout, the browser paints the page to the screen, merging dirty elements into a single large rectangle and repainting it once per frame.

Paint cost is mainly affected by unnecessary repaints.

Reduce unnecessary painting

Even invisible GIFs can trigger paint; hide them with

display:none

. Avoid expensive CSS properties during frequent paints, such as:

<code>color, border-style, visibility, background,
text-decoration, background-image,
background-position, background-repeat,
outline-color, outline, outline-style,
border-radius, outline-width, box-shadow,
background-size
</code>

Reference: csstriggers.com

Reduce painted area

Create separate layers for elements that cause large repaints. The demo below shows the green area being repainted.

Composite

All painted elements are composited. By default they share a single layer; separating them into multiple compositing layers reduces the impact of changes.

The CPU handles most work on the main thread, including:

JavaScript execution

CSS style calculation

Layout calculation

Painting (rasterising)

Sending bitmaps to the compositor thread

The compositor thread is responsible for:

Uploading bitmaps as textures to the GPU

Calculating visible and soon‑to‑be‑visible regions (scrolling)

Processing CSS animations (which run off the main thread)

Instructing the GPU to draw the final bitmap to the screen

The GPU only draws layers, so hardware acceleration greatly improves performance.

Enable hardware acceleration by changing properties such as

opacity

,

transform

, or using

will-change

to hint the browser.

When

transform

or

opacity

change, the browser moves the work to the GPU, allowing fast texture manipulation without involving the main thread’s layout or paint steps.

The

will-change

property explicitly tells the browser to optimise the given property (e.g.,

transform

,

opacity

,

contents

,

scroll-position

). However, overusing it can cause the browser to keep elements in an optimised state continuously, consuming memory and CPU, especially on mobile devices.

GPU bandwidth is limited; creating too many layers or repainting them too often can hit the GPU bottleneck and cause jank. Control both the number of layers and how often they are repainted.

Avoid accidental layer creation, for example by high

z-index

values:

Summary

Achieving silky‑smooth animation depends on two key aspects:

Frame Timing – use

requestAnimationFrame

to let the system schedule frames.

Frame Budget – minimise layout work (read‑then‑write), reduce paint cost (avoid expensive CSS and limit painted area), and apply hardware acceleration wisely.

Performance OptimizationJavaScriptFrontend DevelopmentrequestAnimationFrameweb animation
QQ Music Frontend Team
Written by

QQ Music Frontend Team

QQ Music Web Frontend Team

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.