One weird trick to performant touch-response animations with React

I’ve seen a single fatal performance issue on many sites, including Twitter.com and in high quality UI toolkits including Material UI.

This post includes the TL;DR on the issue and how to solve it, resulting in super performant animations or CSS changes that apply in response to touches.

Overview of the problem

Many sites implement pretty buttons or tabs that respond with some CSS change or animation when tapped. These look great during development when the onclick handler does nothing, and on fast Macbook Pros that can steam quickly through main thread work.

The issue is that these kind of touch-response animations often follow the following pattern:

  1. Detect a touch of some kind
  2. Trigger a CSS toggle, start an animation etc
  3. Call something like an onclick callback that triggers whatever logic the button exists to perform in the app (e.g. change some React state that causes some part of the app to be re-rendererd)

The issue lies in between 2 & 3, as changes to CSS and animations do not actually apply/start synchronously, but rather only start by the beginning of the next frame. This means that if step 3 blocks the main thread for any time, say 300ms, then the CSS change or animation will be blocked for 300ms resulting in your app feeling kludgy and slow.

WAIT!! I can feel some of you think you know this issue and are about to leave, but read on and I promise the solution is probably not what you think

The commonly stated, but incorrect solution

At this point experienced developers will put their magic incantation hat on and say: “I recognize this, we solve it by calling the callback in requestAnimationFrame() which ensures the animation has started (or CSS change is committed) before the callback is even called.

Unfortunately, due to a bug in Safari and Chrome, this will not solve the problem (!)

If your inner voice is screaming right now, don’t worry, mine is too.

You can prove to yourself that this solution fails by creating a small repro where in response to a touch event you toggle a CSS class and then pass a function to requestAnimationFrame that blocks the thread for 1s.

The actual solution

The actual solution is to make the CSS changes or start the animation and then use a double requestAnimationFrame to call any work that may block the main thread. This guarantees that your animation will have started, and will dramatically improve the performance of your application.

The solution will look something like:

requestAnimationFrame(() => {
requestAnimationFrame(onButtonClicked)
})
Now go forth, and make your interactions feel great!

Update (2017/01/24): I’ve now written up some thoughts on whether this actually is a bug or is how things should be following some great discussion on Twitter.