React animations: How a simple component can affect your performance
There are many ways of making animations on the web. In this post, we will explore a naive way of making them and compare it with a better way, all by using React.
Animations on the web
What is the easiest way to define an animation?
Generally, the best way to define an animation is with a mathematical function. For this case, I will keep it simple and say that our function will be a function of time:
You can define more complex animations and functions, for example, one that depends on the previous animation state or some global state (like a game would do). But we will stay with the simplest case.
As an example we are going to animate an
svg element according to a given mathematical function. Since we are going to move the
svg to an
y position it would make sense that our
animation function returns what the styles of that
svg should look like at a given
time, something like:
This example is almost the same as you do with CSS Keyframes, the only difference is that here you need to provide a function that defines every frame, and with Keyframes, you give the essential parts, and the browser fills in the blanks.
::: warning You might be asking yourself: Why bother writing your animations with JS if CSS Keyframes are easier? :::
You have to remember that our goal is to understand the performance aspects of animations. I assume you will use this for complex cases only. For everything else, pure CSS is likely the best choice.
Writing a simple animated React component
Our component will be an SVG Circle that we will move on the screen according to a provided animation function. As a first step, we simply render the SVG.
Now we can use our
Animation component (which is yet to be animated) as follows:
Now that we have our component on the screen, we need to let the time run and calculate the new styles for the
svg using our animation function. A simple solution could be as follows:
Animation component works and animates things pretty well on the screen, but it has some big problems!
Firstly, using a
setInterval that runs every 16ms is CPU intensive, and your users will notice it. Also, it does not care about anything else that is happening on your computer or mobile device. It will try to execute every 16ms even if your computer is struggling, the battery is running low, or the browser window is not visible.
Secondly, that component is going through a React render and commit cycle every ~16ms because we use the internal state of React to store the animation; when we set the state, a render and a commit happens, and that is killing the CPU even more.
You can read more about this on What are render phase and commit phase in react dom? .
Also, if you use the React Dev Tools you can see that the component has a lot of activity. In just a few seconds of profiling, it committed and rendered hundreds of times.
But, since React is so fast and you are probably using a beefy computer, you will not feel any sluggishness on the animation.
You can also record a performance profile on your browser, which for my setup it shows that for every second we are animating, we are using our CPU/GPU ~11% of the time.
Now, let's see how to do it better.
Writing a performant animated React component
We start very similarly to the previous implementation. But you will notice we are not using React's
useState hook, and that is because for this implementation after the animation gets started, we don't care about the state of the component. Our objective is to be as fast and efficient as possible.
We are going to be writing to the DOM outside of React render and commit cycle, React is still going to be useful, because it provides the API for setting up the scene, that is mounting, unmounting the element to/from the DOM and the
useEffect hook to get things started.
The next step is to use the
useRef hook and get a handle to the SVG element after it is mounted so we can do the DOM updating ourselves.
Next, we will use the
useEffect hook to synchronize our component with the DOM state. When the element is mounted, and after we have a reference, we create a
animateFn which takes the time provided by the
requestAnimationFrame function and calculates the next animation state. I am assuming you know what
requestAnimationFrame is. If you don't, please refer to the documentation.
The previous snippet has two key differences from the first implementation. The first one is that we use
requestAnimationFrame, which allows us to be conscious of the user's machine state. In other words, it lets the browser decide when to run the animation and at what FPS. That will save CPU time, battery and will likely make animations smoother.
The second important part is that instead of using
useState to save the animation and let React handle the rendering, we update the DOM ourselves. And that avoids the React commit and render loop from executing at all, saving CPU time.
If you look at the React Dev Tools, you will notice that this component is only committed and rendered once even though it runs the animation.
By looking at the browser performance profile, the CPU/GPU usage is ~9% for every second of animation. It does not sound like a significant change, but this is just one small component. Imagine doing the same with a real application that has hundreds of components. You can try it yourself at the demo application
As with everything in life, there are tradeoffs. The biggest one for this case, in my opinion, is that the first implementation was simple and easy to read. If you know the basics of React, you could understand it. The second one not so much, you need to understand React and the browser in more depth. Sometimes this is acceptable. On the other hand, the first implementation was very inefficient, the second one is very fast, and that is the most significant tradeoff.
And finally, if you need a framework to decide when to use CSS or JS to animate things, I would start by asking the following questions:
- Does my animation need some kind of state?. If no, then CSS is probably the way to go.
- Do I need control of "every frame"? If the answer is no, then CSS Keyframes is worth trying.
And before you go and animate everything yourself, check out the framer-motion package. It will likely cover most of your needs.