In this article, learn some CSS “hacks” to get 60fps animations in React.js. If you haven’t been using them already, you might kick yourself once you see they can be the difference between jittery and silky smooth animations.

Since 2012, every major browser vendor (Safari/Chrome/Firefox/Edge) implemented something called “hardware acceleration”. In short, browsers saw the opportunity for the GPU processor to help handle the ever-increasing workloads that new CSS3 features, <video> , and <canvas> would require. Many of these new features relied heavily on image processing tasks which the CPU just wasn’t designed to do, and would have difficulty maintaining 60fps.

60fps means that each second you're looking at a website, what you're actually looking at is 60 images flash in front of you. That's a whole lotta of work for your computer to do.

Since JavaScript is a single-threaded language, when your CPU is busy handling animations that require a lot of computing resources you’ll begin to see the tell-tale signs of jank… the website will jump around when you scroll, typing text into an <input> box is laggy… That sort of thing. Hardware acceleration allows your browser to intelligently offload these pesky CPU hogging tasks to the GPU so that the main thread of the CPU isn’t blocked.

As a React.js developer, you can leverage hardware acceleration by animating these CSS properties:

transform which includes translateX , translateY , scale , rotate , etc

which includes , , , , etc opacity ; we all know what this does 🙂

This One Goes to 11

Below is a stress test that animates an image up-and-down (along the Y-axis). There’s two ways to do this. One using absolute positioning and animating the top property. And the other (better) way uses hardware acceleration by animating transform: translateY() .

Depending on how powerful your device is, you should see jittery animations on the first version because it’s animating the top property which is handled by the CPU. In the optimized version, the animation moves fluidly by animating the CSS transform property which is handled by the GPU. That’s only difference between the two… Crazy right?!

// Using styled-components for the CSS // https://github.com/styled-components/styled-components // // But you can rework this with plain ole inline styles // using CSS "transition" instead of @keyframes const unoptimizedCSS = keyframes` 0%: { top: 20px; } 50% { top: 250px; } ` const optimizedCSS = keyframes` 0%: { transform: translateY(0px); } 50% { transform: translateY(250px); } `

You may be thinking, “wow this amazing and you’re so smart William but… just transform and opacity ? Isn’t that kinda limiting?” Ah-ha… not so! Many real-world UI animations can be done with them.

The Perf is in the Pudding

Check out these 3 common UI animations that only rely on transform and opacity . The first is a toast notification.

Toast

<div style={{ position: 'fixed', display: 'flex', width: this.toastWidth, height: this.toastHeight, color: 'white', transform: `translateY(${this.state.isVisible ? -this.toastTravelDistance : 0}px)`, opacity: this.state.isVisible ? 1 : 0, transition: 'transform 500ms ease-out, opacity 300ms 100ms linear', }} > <!-- Contents of the toast notifification --> </div>

The toast <div> simply animates from the bottom edge of the browser window. And yep. It’s just transform and opacity ! Even if you had several of these toast notifications on the page, the animations would still be fluid because it’s leaning on the GPU for all of the heavy animation work. No dropped frames. No funny scrolling problems.

Hidden drawer

In this second example, clicking on the burger icon reveals a drawer.

<div> // Main page <div style={{ transform: this.state.isDrawerVisible ? 'perspective(800px) translateZ(-40px)' : 'none', transition: 'transform 500ms ease-out', }}></div> // Drawer <div style={{ position: 'absolute', top: 0, left: -this.drawerWidth, display: 'flex', width: this.drawerWidth, transform: `translateX(${this.state.isDrawerVisible ? this.drawerWidth : 0}px)`, opacity: 1, transition: 'transform 750ms 250ms ease-out', }} > </div> </div>

Both the drawer and the main part of the app animate using transform . The drawer animates from outside the viewable area using transform: translateX() , and the main page animates transform: translateZ() to push it back slightly. That’s probably unnecessary, but looks pretty dang cool. Lastly, we have a dropdown menu.

Dropdown menu

<div> <!-- button --> <button onClick={() => this.setState({isVisible: !this.state.isVisible})} style={{ color: 'white', background: '#008f68', fontWeight: 'bold', fontSize: 23, }} > <div>About Us</div> <!-- rotating triangle --> <div style={{ borderRadius: '100%', transform: `rotateZ(${this.state.isVisible ? 180 : 0}deg)`, transition: 'transform 250ms linear', }}> <span style={{ position: 'relative', top: 14, width: 0, height: 0, borderStyle: 'solid', borderWidth: '10px 8px 0', // CSS hack for a triangle shape borderColor: '#066349 transparent transparent transparent', }}/> </div> </button> <!-- hidden/visible menu --> <div style={{ width: 280, height: 280, fontWeight: 'bold', overflow: 'hidden' //👈 hides the menu contents }}> <div style={{ position: 'relative', top: -280, width: 280, height: 280, background: '#10aa80', transform: `translateY(${this.state.isVisible ? 280 : 0}px)`, transition: 'transform 500ms ease-out', }}><!-- dropdown contents --></div> </div> </div>

The menu reveals itself by animating transform: translateY() . And if you look closely the triangle icon also rotates. This is done with transform: rotateZ() . All of this stuff is hardware accelerated!

Conclusion

On the web, we can get animations that look as fluid as native apps through the magic of hardware acceleration! If you were hesitant to use CSS animations in your projects because you saw them as performance liabilities, try using transform and opacity in your animations. Your app’s performance won’t take a hit, and you’ll offer a more engaging experience for your users.

Resources