How to Use requestAnimationFrame with React
Aug 3, 2020 • 8 Minute Read
Introduction
Animations are key to developing interactive and engaging websites. A simple animation can be created with setInterval or setTimeout functions to change the properties of the elements after a defined interval. Some of the common animations like fade in/out, slide in/out, or collapse/expand are often used to display a change on a page to provide a meaningful transition effect to the user.
Animating an element involves reflow (changes in height, width, font, etc.) and repaint (changes in element color, background, etc.), which occur due to changes in the Document Object Model (DOM) to render or update a page. Faulty or slow animation can cause a sluggish effect on the screen due to multiple invocation of reflow/repaint processes.
Animations can be created using CSS but it is not suitable for non-linear animation that requires input from various sources such as users or functions. This guide covers the use of the requestAnimationFrame method to create optimal animations in react. The codebase is available on github.
Rerendring Process in React
A rendering process includes the calculations of layout and style changes in DOM to update the page. DOM represents an HTML document in terms of nodes, and each node can further have information or style (from CSS) nodes. Changing DOM is expensive and time-consuming, so React uses a virtual DOM to track the changes and then only apply the new changes in the actual DOM by using a diffing algorithm.
A series of frames make an animation, and each frame is being rendered on the screen one after another. As a rule of thumb, animation should be able to render 60 frames per second for smooth experience, but the browser or system may not always be able to process 60FPS. Consider the example when a user has opened 20-30 links simultaneously.
Usage of requestAnimationFrame
A simple animation can be created using setTimeout and setInterval. The setTimeout is executed after a provided delay and needs to be called recursively whereas setInterval repeatedly executes the code after the defined interval:
// move element down by 3px, 60 times in a second
import React, { Component } from 'react';
import { render } from 'react-dom';
import './App.css';
export default class App extends Component {
constructor() {
super();
this.state = {
name: 'Football Animation'
};
}
// change the top position by 3 pixels of an element that has a “circle” class value.
moveBall = () => {
let start = Date.now();
let football = document.querySelector(".circle")
let timer = setInterval(function () {
let interval = Date.now() - start;
football.style.top = interval / 3 + 'px'; // move element down by 3px
if (interval > 1000) clearInterval(timer); // stop animation
}, 1000 / 60);
}
render() {
return (
<div className="containter">
<img className="circle" onClick={this.moveBall} />
</div>
);
}
}
.App {
width: 100vw;
height: 100vh;
}
.circle {
left: 0;
right: 0;
margin: 0 auto;
position: fixed;
width: 75px;
height: 75px;
background-position: center;
background-image: url("https://img.icons8.com/officel/80/000000/football2--v2.png");
background-color: #ffffff;
border-radius: 50px;
background-repeat: no-repeat;
}
The above snippet will move the element that has the circle class value down, which works fine but there are some drawbacks:
- setInterval is not guaranteed to be called at the given delay due to a slow or busy system.
- Using a precise interval can also lead to unnecessary reflow or repaint cycles. For example, changing the page after every 16.66 milliseconds will force the system to process 60 FPS but the browser or system may not be able to execute this due to a busy state. Furthermore, it's useless to consume resources on a page when the user is on a different tab.
To optimize system and browser resources, it is recommended to use requestAnimationFrame, which requests the browser to execute the code during the next repaint cycle. This allows the system to optimize resources and frame-rate to reduce unnecessary reflow/repaint calls.
Animation Implementation Using requestAnimationFrame
The requestAnimationFrame method only calls the input animation function when the browser is ready to perform the paint operation. The earlier example of the moveBall function can be implemented simply by replacing setInterval with requestAnimationFrame:
moveBall = () => {
let start = Date.now();
let football = document.querySelector(".circle")
// timestamp: time elapsed in milliseconds since the web page was loaded
let timer = requestAnimationFrame(function animateBall(timestamp) {
let interval = Date.now() - start;
football.style.top = interval / 3 + 'px'; // move element down
if (interval < 1000) requestAnimationFrame(animateBall); // queue request for next frame
});
}
The requestAnimationFrame method only executes the callback function once per request and needs to be called again with requestAnimationFrame(animateBall) to perform the next animation frame transition. The requestAnimationFrame method returns an integer ID which can be used to cancel the queued request using the cancelAnimationFrame(id) method. The animation callback function can also accept a timestamp value, which is the time elapsed in milliseconds since the web page was loaded. It provides the exact timestamp when the callback is executed.
Implementing Bounce Animation
Let's implement the bounce animation to make the ball animation more interesting and natural. To implement the bounce, two methods are required, calculating the bounce factor for y axis:
bounce = (timeFraction) => {
for (let a = 0, b = 1; 1; a += b, b /= 2) {
if (timeFraction >= (7 - 4 * a) / 11) { // 4 and 7 coefficient are used to control bounce and smooth y axis fall
return -Math.pow((11 - 6 * a - 11 * timeFraction) / 4, 2) + Math.pow(b, 2) // Math.pow(b, 2) to keep the same x axis for bounce
// -Math.pow((11 - 6 * a - 11 * timeFraction) / 4, 2) adjust the y axis up and down
}
}
}```
and the `easeOut` function that takes an animation methods and returns a wrapper method to convert the bounce y-axis factor downwards:
```JS
easeOut = (timing) => {
return (timeFraction) => {
return 1 - timing(1 - timeFraction);
}
}
Now the bounce function will look like below:
bounceBall = () => {
let bounceEaseOut = this.easeOut(this.bounce);
let start = Date.now();
let football = document.querySelector(".circle")
let id = requestAnimationFrame(function animate(time) {
let interval = (Date.now() - start) / 2000;
if (interval > 1) interval = 1;
football.style.top = bounceEaseOut(interval) * 300 + 'px' // adjust the y axis
// football.style.left = interval * 200 + 'px' // adjust the x axis
if (interval < 1) {
requestAnimationFrame(animate);
}
})
}
Conclusion
- The requestAnimationFrame is supported by all modern browsers, and support goes back to IE10.
- For GPU intensive tasks, use transform methods when possible for better performance.
- requestAnimationFrame also allows the browser to block the animations to save battery when the page is not active or the system is under-performing.
- Favor using transform properties instead of height and width property changes to avoid initiating repaint for the whole page.
The optimized codebase is available on my bounce-react-animation repository. This guide explained the necessary details to get started with animations in React with requestAnimationFrame. Happy coding!