React at 60fps

Written by okonetchnikov | Published 2017/01/13
Tech Story Tags: react | javascript | performance

TLDRvia the TL;DR App

React is an abstraction over the DOM and as any abstraction, it has its costs and limitations that you may hit sooner or later. Understanding and being able to overcome such limitations are important parts of working with an abstraction.

There is a belief that React is fast out of the box and to some degree it is true — most of the time building user interfaces with React you may not think of optimizing for performance. Sometimes though, to achieve better performance (and better user experience) one has to think out of the box.

I’d like to share some techniques I’ve been using when working with React.

Don’t optimize prematurely!

A word of warning before we even begin. No matter what you do, please don’t optimize prematurely for performance. This means, don’t do anything until you have evidence that you have a performance problem. With React, optimizing too much might lead to weird bugs.

Every performance optimization process should look like this:

  1. Realise there is a performance issue
  2. Measure using DevTools and analyze to find the bottleneck
  3. Work around the issue
  4. Test again to confirm an improvement
  5. Goto 2 if needed

By the way, React 15.4 introduces the new performance tool that integrates nicely with Chrome’s DevTools and makes it easier to locate slow components in the render tree.

Now let’s dive in into some common and not so common React-related performance optimizations techniques I’ve been using on various projects.

To shouldComponentUpdate or not?

Many are familiar with React’s life-cycle method called shouldComponentUpdate. This method returns a Boolean value depending on which React will skip the render method call for the component that implements this method.

As I started working with React a few years ago I naïvely assumed that the library will automatically optimize rendering by not calling render if state and props didn’t change. As a matter of fact, by default this method doesn’t do anything and thus React.js always triggers a re-render when you call this.setState or a component receives new props.

Implementing the shouldComponentUpdate method is probably the simplest way to make a slow component faster but this method has some pitfalls. By bailing out of rendering high in the component tree, you might miss a required re-render further down the tree since it will skip rendering for all child components of the component where the method is defined. The most common implementation like shouldPureComponentUpdate that shallowly compares next and current state and props also can be tricky:

  1. It won’t compare deeply nested objects (and comparing deeply nested objects is slow — this is why you should consider using immutable data structures)
  2. Passing new instances of callback functions via props will also make this function always return true. Tip: use ESLint with eslint-plugin-react to catch it.
  3. Checks aren’t free. Doing lots of checks can slow down your application.

In practice this means that in most cases it should only be used for:

  1. Pure components that use simple props (no deep objects or arrays in props)
  2. “leaf”-components or components located deep in the rendering tree.

That’s why I think that even before you start implementing the shouldComponentUpdate on your classes (or, if you prefer functional components, composing with pure HoC), you should analyze and find out what component makes the app slower.

Move expensive code to a higher level component

If you have some expensive calculation of derived data in the render method you might reduce the number of calls by an order of magnitude by off-loading these calculations to a higher-level component or memoizing the result. Using libraries like reselect can be a huge help.

During my work on https://status.postmarkapp.com I was able to improve the hover performance for service metrics graphs by:

  1. Splitting the visualization and the overlay info into separate components
  2. Off-loading the expensive data transformation to the wrapper component
  3. Implementing shouldComponentUpdate methods for both visualization and overlay components
  4. Using immutable data structures so the comparison is less expensive

ServiceMetric Component hover example

Often though, one of the slowest parts of applications I’ve worked on was triggering DOM manipulation following user input. Reacting to scroll or mouse events is a great way to slow down your application. By nature such events can fire at a very high rate. And since browser only has 16ms to do all the work to run at 60fps, reacting to each of those events can completely block your JS application.

Often the debounce pattern is used to prevent these drops in performance. They indeed reduce the number of callback calls but at the same time also make our UI feel less responsive to user input. Can we still react to mouse events and stay within the performance budget?

Synchronized scroll component example

To illustrate the process, I’ve built the synchronized scroll component similar to what I made for the Netlify CMS post editor:

The component should keep the scrolling positions of the two panes in sync. Since the height of the content of each pane can be different, the component needs to scroll the panes at different speeds. It should also work with any other components and be easy to integrate regardless of your application structure.

Don’t abuse this.setState

One of the common mistakes in React applications I’ve seen often is the usage of the this.setState method for storing internal DOM state in the component.

If you don’t use something in render(), it shouldn't be in the state.

Now consider the following example which was the first implementation of SyncronizedPane Component I came up with:

It’s tempting to put all the state into the this.state (because of its name, I guess). The problem with this is that each time you call this.setState to change it, React will re-render the whole consequent tree of elements that might cost a lot of CPU time.

Slide from https://speakerdeck.com/vjeux/react-rally-animated-react-performance-toolbox

The question is: do you really need to pass the scrollTop value as a prop down the component’s tree using React life-cycle? Many forget that you still can store the arbitrary state in an instance variable. In the example above, doing

will not trigger a re-render. But how do we update the scroll position of the underlying component then? The trick here is to do it manually.

— What?! That’s not declarative code anymore! — you might shout at this moment.

No, it is not! And neither it is idiomatic React!

A nice thing about React is that by using the context you still can write imperative code or access the DOM directly but hide it from other components so that the rest of application’s code will still remain clean and declarative.

Context allows creating child-parent relationships between components. This means our children components can gain access to some state or even methods of the parent component.

Direct DOM Manipulation

So, taking the previous example, we could re-write it like (simplified version):

So what’s happening here?

  1. The ScrollContainer component implements register / unregister methods that are meant to add / remove panes and attach / detach event listeners
  2. The ScrollPane component does very little now: it only calls register and unregister at mount and unmount time
  3. Each time one of the panes fires an onScroll event, the callback gets triggered, which calculates and sets new scrollTop positions for all of the panes.

See how we completely skip using this.setState and passing props down the tree. This allows updating the scroll positions of the DOM nodes without triggering the CPU-intensive virtual DOM operations of React. Besides that, a few more interesting things happening there:

  1. We now can use ScrollPane at any place in our application and not just as a child of ScrollContainer.
  2. All ScrollPane components register themselves in the container component by passing references to their DOM nodes. This makes the calculation and manipulation of the scrollTop properties trivial.
  3. We can now have any number of “panes” with the synchronized scroll.

You can find the full code for the working component on GitHub: https://github.com/okonet/react-sync-scroll and a working demo and documentation here: http://react-sync-scroll.netlify.com/.

Turns out, this technique is also being used in React Native’s Animated. See the slides of the presentation by @vjeux: https://speakerdeck.com/vjeux/react-rally-animated-react-performance-toolbox

Conclusion

There are many best practices and patterns that can lead to a better application architecture and you should definitely follow them until the user experience of the application starts suffering. Sometimes, doing things in a less idiomatic way or not always following the “React way” can lead to a better user experience.

Finding a balance between good performance and code maintainability is tricky but this is a part of every UI-developer’s job. Ultimately, we build software not for the sake of following patterns, but for people.

Related reading and watching:

  1. Performance Engineering With React
  2. On the Spectrum of Abstraction by Cheng Lou
  3. React Native: Building Fluid User Experiences by Spencer Ahrens
  4. Slides from Animated! by @vjeux

Thanks Karl Horky for editing and Max Stoiber for the review.


Written by okonetchnikov | Improving designers & developers communication
Published by HackerNoon on 2017/01/13