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.
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:
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.
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:
props
will also make this function always return true
. Tip: use ESLint with eslint-plugin-react to catch it.
In practice this means that in most cases it should only be used for:
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.
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:
shouldComponentUpdate
methods for both visualization and overlay components
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?
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.
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.
So, taking the previous example, we could re-write it like (simplified version):
So what’s happening here?
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:
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
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.
Thanks Karl Horky for editing and Max Stoiber for the review.