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.
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
setTimeoutis 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,
setTimeoutcallbacks 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,
setTimeoutcontinues 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
offsetTopforces 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.jsautomate 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-changeto hint the browser.
When
transformor
opacitychange, the browser moves the work to the GPU, allowing fast texture manipulation without involving the main thread’s layout or paint steps.
The
will-changeproperty 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-indexvalues:
Summary
Achieving silky‑smooth animation depends on two key aspects:
Frame Timing – use
requestAnimationFrameto 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.
QQ Music Frontend Team
QQ Music Web Frontend Team
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.