Overview

HighPerformanceReact.dev

HighPerformanceReact.dev is a collection of tips and tricks to help you write high performance React code.

My name is Tom and I have over seven years' experience building React apps. I also wrote The Computer Science Book, a complete introduction to computer science in one book. It’s aimed at self-taught developers who want to understand what’s really going on under the hood.

HPR is currently a work in progress. Sections marked “WIP” as to be completed. Development is in public on Github. If you have any suggestions or corrections, please open an issue on Github.

References to the React source code are using commit 9cdf8a9.

Always remember the rules for writing high performance code:

  1. First make the code work.
  2. Identify which code is harming performance.
  3. Optimise and measure the outcome.

Do not worry about making your code fast before you have it working.

Pre-requisites

The first step to improve performance is to understand performance. We need to understand how React works and how application performance is measured.

Understand React

React updates components in three steps:

  1. Render: React calls your component and gets a React Element in return.
  2. Reconciliation: React compares the new Elements from step 1 with those from the previous render.
  3. Commit: If there are changes, React updates the DOM to reflect the new changes.

React is designed on the assumption that re-rendering components (step 1 above) is cheap but updating the DOM is expensive. While this is a good rule of thumb, bear in mind that a rendering component will render all of its children too. It’s therefore possible to inadvertently perform a lot of unnecessary work.

Optimising React apps is largely about speeding up slow renders and eliminating unnecessary re-renders.

[WIP: maybe clearer to use “update” for React rendering to separate it from DOM changes?]

Understand component updates (WIP)

A React component will update when:

  1. Its parent re-renders
  2. Its state changes
  3. A Context it uses has changed
  4. A hook triggers schedules an update
  5. The component is memoised and a prop value has changed

Profile your app

It is essential to understand your browser’s JS profiler and the profiler included in React Developer Tools.

Your browser’s renderer has detailed information but can only observe plain JS execution. The React profiler can tell you which components are rendering and why. The React profiler is pretty easy to use once you know how.

How to record a profile

  1. Install React Developer Tools and then open the Profile tab in your browser’s dev tools
  2. Click the gear button, go to ‘Profiler’ tab and enable ‘Record why each component rendered while profiling’
  3. Click the red ‘record’ button in the top left, interact with your app and then stop recording.

How to read a profile (WIP)

At the top right we have the list of commits React performed during the profile. The higher the bar, the longer that commit took.

The main pane shows which components updated during the selected commit. At the top we have the root component and leaves at the bottom. Take some time to work out how the profiler output corresponds to your app’s component structure.

The width of each bar indicates how long that component and its children took to render. ‘Hotter’ colours, such as yellow and red, indicate slower components. Grey means that the component didn’t update at all in this commit.

As you hover over each component, the tooltip will tell you why the component updated. This is super useful to identify which props are triggering updates. Sadly, the explanation is often inaccurate. You’ll almost certainly find cases where the profiler says that a component updated because its parent re-rendered while also claiming that the parent didn’t re-render. This is because the profiler uses The parent component rendered. as its default explanation.

When the profiler tries to tell you nonsense, stand your ground! Look carefully at the troublesome component and see what else could have triggered an update. I usually find it involves a hook.

The basics

These tips cover basic techniques that all React developers should be familiar with. Make sure that you have properly implemented these before trying any fancier optimisations!

If you’re already familiar with React, you may want to skip ahead.

Fix key warnings

Keys are special props used to uniquely identify a component. React will warn when you render a list of components without providing keys. Check in your browser console for any warnings and fix them.

React’s reconciliation algorithm relies on keys to identify list items that have not changed. Imagine you have a NavBar component:

<NavBar>
  <NavItem path="/about" label="About"/>
  <NavItem path="/buy-my-book" label="Buy My Book!"/>
</NavBar>

Now you add functionality for a logged-in user to access their account:

<NavBar>
  {user.loggedIn && (<NavItem path="/my-account" label="My Account"/>)}
  <NavItem path="/about" label="About"/>
  <NavItem path="/buy-my-book" label="Buy My Book!"/>
</NavBar>

When the user logs in and React adds the NavItem, it will compare the new first element with the previous first element and find they don’t match. React will recreate every NavItem, even though two of them have not changed. We must provide stable, unique keys so that React can “see” that two NavItems remain unchanged and it only needs to create a single new one.

It’s surprisingly easy to introduce key warnings as you refactor code. The key must always be defined on the component that is directly rendered by the list. In the example above, if we wrapped the NavItems in another component, we’d need to move the keys to the wrapping component for them to have any benefit.

Memoising function components (WIP)

React executes your entire functional component every time it re-renders.

Don’t recompute data or callbacks

useMemo for expensive computation.

useCallback so that callback props have a persistent reference value.

Use React.memo to avoid re-renders

React will normally avoid re-rendering a component wrapped in React.memo while its props do not change.

Implement shouldComponentUpdate on class components

[WIP: create pre-requisite section on deep vs shallow equality checks?]

React will by default re-render your component when its parent re-renders. The lifecycle method shouldComponentUpdate is used to prevent re-renders when you know that the component has not changed.

React calls shouldComponentUpdate before rendering whenever new state or props have been received. React’s default behaviour [WIP: add link] is to always re-render.

You can provide a custom implementation of the shouldComponentUpdate method receiving the new and previous values of both props and state. You can compare them and return false when a re-render is unnecessary.

How can you tell when a re-render is unnecessary? When the component’s rendered output is a pure function of its props and state, a re-render is only required if a prop or state value has changed.

[WIP: example]

When using an immutable data layer (e.g. Redux), you can perform a fast shallow equality check using === to quickly check whether object references have changed (see PureComponent below).

Checking nested objects for changing values is known as deep equality checking and has its own cost. When you have complex, mutable data, I recommend against trying to write some fancy deep equality check unless you’ve profiled the app and determined that rendering time saved outweighs the cost of the deep equality check.

Consider adding to data a unique identifier which is updated every time the data is mutated. Then in shouldComponentUpdate you only need to compare the identifiers.

PureComponent

PureComponent is a variant of the standard React Component class that behaves as if it had a shouldComponentUpdate implementing a shallow equality check of props and state.

Looking at the PureComponent implementation, we can see that it doesn’t actually work by providing a shouldComponentUpdate implementation, but it achieves the same effect.

PureComponent will stop your component re-rendering just because the parent re-rendered. Shallow equality checking will not prevent re-renders when object references changes, even if the new object holds the same values as the old one (see here). Using mutable data will cause PureComponent to not re-render when it should, since the object references will not have changed.

Prefer to use PureComponent when your component’s rendered output is a pure function of its state and props.

Intermediate

These are more advanced steps to be used when your application is working correctly and you have identified performance bottlenecks. They frequently cause unnecessary re-renders and can be fixed without affecting behaviour.

Props must pass referential equality checks (WIP)

Your component will re-render if a prop value changes. Even with PureComponent or React.memo, changing function and object references can cause unexpected re-renders:

const MemoisedButton = React.memo(Button);

const Component = props => (
  <MemoisedButton onClick={event => props.onClick(event)}
)

You might think that memoising the Button will prevent it rendering when Component renders. In fact, the definition of onClick creates a new lambda every time. Each lambda will have a different reference and React.memo will think that the prop value has changed and will re-render.

In this case, the lambda provides no benefit and removing it suffices to fix the problem.

When you need to pass extra parameters into the callback, consider creating an intermediate component that receives the extra parameters as props and can construct a memoised callback:

const MemoisedButton = React.memo(Button);

// Each button has a different value to pass to the callback
const ButtonsUnoptimised = props => (
  <>
    <MemoisedButton onClick={event => props.onClick(event, "first")} />
    <MemoisedButton onClick={event => props.onClick(event, "second"}} />
    <MemoisedButton onClick={event => props.onClick(event, "third")} />
  </>
)

const NamedButton = props => {
  const handleClick = React.useCallback(
    event => props.onClick(event, props.name),
    [props.name]
  );
  
  return (
    <MemoisedButton onClick={handleClick} />
  );
};

const MemoisedNamedButton = React.memo(NamedButton);

const ButtonsUnoptimised = props => (
  <>
    <MemoisedNamedButton onClick={props.onClick} name="first" />
    <MemoisedNamedButton onClick={props.onClick} name="second" />
    <MemoisedNamedButton onClick={props.onClick} name="third" />
  </>
);

Don’t bother trying to use Function.prototype.bind to construct partially-applied callbacks. They will still fail shallow equality checks.

Object literals passed in as props will be recreated on each render:

const MemoisedButton = React.memo(Button);

const Component = props => (
  <MemoisedButton onClick={props.onClick} styles={{ background: "red" }} />
);

Each styles prop value will have a different object reference, and so failing React.memo’s shallow equality checks, even though the content does not change.

Be careful when mapping over arrays (WIP)

Don’t change tag type (WIP)

React will always re-render the entire component if you change its root tag, even if everything else is the same [WIP: source link].

Avoid components that regularly change their root component based on state or props.

Don’t create new components within render (WIP)

Do not create new component definitions within the render path of another:

const Component = props => {
  const Wrapper = wrapperProps => (
    <div onClick={props.onClick>{wrapperProps.children}</div>
  );
  
  return (
    <Wrapper {...props} />
      // content
    </Wrapper>
  )
};

React will always think that each instance of Wrapper is an entirely new component and will always re-render the entire component tree [WIP: source link here].

[WIP: what happens if we add a key to Wrapper?]

You might think that the example above is an obvious code smell. It is possible to hit this problem in a less obvious way using higher-order components:

import { someHoC } from "./some-hoc";

const Component = props => {
  const WrappedComponent = someHoC(OtherComponent);
  
  return (
    <WrappedComponent {...props} />
      // content
    </WrappedComponent/>
  )
};

Always wrap components in higher-order components at the file level so that only one component definition is created:

import { someHoC } from "./some-hoc";

const WrappedComponent = someHoC(OtherComponent);

const Component = props => {
  return (
    <WrappedComponent {...props} />
      // content
    </WrappedComponent/>
  )
};

Move constants out of components (WIP)

Computed values that do not change should be defined outside of the component. Style objects are a common example:

const Component = props => {
  const styles = { fontSize: 24 };
  return (
    <Button {...props} styles={styles} />
  )
};

There are two issues here:

  • redefining the styles object will fail shallow equality comparisons, causing Button to re-render unnecessarily
  • the object is computed every time Component renders even though it never changes, wasting computation.

Though the amount of wasted work is tiny in this example, it is easy to accidentally waste significant time filtering arrays, reformatting data etc.

When the data depends partially on a prop or state value, use the useMemo hook to cache the data as much as possible.

Advanced

These are expert-level techniques that should only be used when there is a clear performance benefit that outweighs any implementation complexity.

Know where to store state without re-rendering (WIP)

Sometimes it is useful to track state without triggering a re-render whenever that state changes.

For example, you may want to track whether the cursor has hovered over a subcomponent and use that data in a click handler.

In a class component, you can simply write to an instance property on the component class. You are of course responsible for keeping that instance property in sync with prop and state changes.

In functional components, you can (mis)use the useRef hook to store data without triggering a re-render. I have used this technique to keep a click handler in sync with changes caused by mouseover events. Without useRef, the click handler will hold a reference to a callback with a stale closure:

[wip example code]

Understand useState

useState is one of the most common hooks. Understand how it works to avoid unnecessary anti-patterns.

Don’t check for changes before updating

The useState implementation [wip: code link] already checks whether the value has changed before doing anything. Implementing your own check is unnecessary and does not improve performance (though it has no additional cost either).

Access current state in the update handler

This example code sets the visibility of a modal:

const ModalWrapper = props => {
  const [isVisible, setIsVisible] = useState(false);
  const handleClick = useCallback(
    () => setIsVisible(!isVisible),
    [isVisible]
  );

  return (
    <ModalContent onClick={handleClick} show={isVisible} />
  );
};

It attempts to cache the click handler to aid React.memo’s memoisation and avoid re-renders.

However, the callback depends on isVisible and so it will be recreated every time isVisible changes value! This is more or less as bad as not memoising at all.

Update handlers provide the current state as the first parameter, so use that instead of recreating the callback:

const ModalWrapper = props => {
  const [isVisible, setIsVisible] = useState(false);
  const handleClick = useCallback(
    () => setIsVisible(v => !v), // change here
    [] // and here
  ); 

  return (
    <ModalContent onClick={handleClick} show={isVisible} />
  );
};

Know when updates are batched (WIP)

React by default batches state updates so that multiple state updates are applied together and only cause a single re-render.

This does not apply in all cases. Most importantly, state updates in asynchronous calls are not batched.

[WIP code example]

Understand unmounting versus hiding (WIP)

Ensure that contexts are used carefully (WIP)

A change to a context value will cause every consumer of that context to re-render.

The same concerns around prop equality checks therefore apply to contexts too.

Snippets to detect changes (WIP)

Sometimes (often), React Dev Tools gives you inaccurate or incomplete information about why a component has re-rendered. The following snippets will iterate through all state and prop items and log which ones changes.

I find them useful to use on occasion.

Class component

Function component