Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?

Early draft, please don't publish until the performance improvements are merged and shipped.


React 16.5 recently shipped, which added support for some new Profiling tools. We recently used these tools to identify a major source of slow render performance.

Faithlife.com is a web application powered by React 16.3. The homepage consists of a reverse-chronological timeline of posts. We received some reports that interactions with posts (such as replying) caused the browser to lag, depending on how far down the post was on the page. The further down the page the post was, the more lag occurred.

After updating React to 16.5 on a local copy of Faithlife, our next step was to start profiling and capture what components were re-rendering. Below is a screenshot of what the tools showed us clicking the 'Like' button on any post:

Slow renders screenshot

The blue blocks below NewsFeed show render being called on all the posts in the feed. If there were 10 items loaded, NewsFeedItem and all its children would get rendered 10 times. This can be fine for small components, but if the render tree is deep, rendering a component and its children unnecessarily can cause performance problems. As a user scrolls down on the page, more posts get loaded in the feed. This causes render to get called for posts all the way at the top, even though they haven't changed!

This seemed like a good time to try changing NewsFeedItem to extend PureComponent, which will skip re-rendering the component and its children if the props have not changed (a shallow comparison is used for this check).

Unfortunately applying PureComponent was not enough - profiling again showed that unnecessary component renders were still happening. We then uncovered two issues preventing us from leveraging PureComponent's optimizations:

First roadblock: Use of children props.

We had a component that looked something like this:

<NewsFeedItemWithHandlers contents={item.contents}>
  <VisibilitySensor itemId={item.id} onChange={this.handleVisibilityChange} />
</NewsFeedItemWithHandlers>

This compiles down to:

React.createElement(
  NewsFeedItemWithHandlers,
  { contents: item.contents },
  React.createElement(VisibilitySensor, { itemId: item.id, onChange: this.handleVisibilityChange })
);

Because React creates a new instance of VisibilitySensor during each render, the children prop always changes, so making NewsFeedItem a PureComponent would make things worse, since a shallow comparison in shouldComponentUpdate may not be cheap to run and will always return true.

Our solution here was to move VisibilitySensor into a render prop and use a bound function:

<NewsFeedItemWithHandlers
  contents={item.contents}
  itemId={item.id}
  handleVisibilityChange={this.handleVisibilityChange}
/>

class NewsFeedItemWithHandlers extends PureComponent {
  // The arrow function needs to get created outside of render, or the shallow comparison will fail
  renderVisibilitySensor = () => (
    <VisibilitySensor
      itemId={this.props.itemId}
      onChange={this.handleVisibilityChange}
    />
  );

  render() {
    <NewsFeedItemWithHandlers
      contents={this.props.contents}
      renderVisibilitySensor={this.renderVisibilitySensor}
    />;
  }
}

Because the bound function only gets created once, the same function instance will be passed as props to NewsFeedItem.

Second roadblock: Inline object created during render

We had some code that was creating a new instance of a url helper in each render:

getUrlHelper = () => new NewsFeedUrlHelper(
	this.props.moreItemsUrlTemplate,
	this.props.pollItemsUrlTemplate,
	this.props.updateItemsUrlTemplate,
);

<NewsFeedItemWithHandlers
	contents={item.contents}
	urlHelper={this.getUrlHelper()} // new object created with each method call
/>

Since getUrlHelper is computed from props, there's no point in creating more than one instance if we can cache the previous result and re-use that. We used memoize-one to solve this problem:

import memoizeOne from 'memoize-one';

const memoizedUrlHelper = memoizeOne(
	(moreItemsUrlTemplate, pollItemsUrlTemplate, updateItemsUrlTemplate) =>
		new NewsFeedUrlHelper({
			moreItemsUrlTemplate,
			pollItemsUrlTemplate,
			updateItemsUrlTemplate,
		}),
);

// in the component
getUrlHelper = memoizedUrlHelper(
	this.props.moreItemsUrlTemplate,
	this.props.pollItemsUrlTemplate,
	this.props.updateItemsUrlTemplate
);

Now we will create a new url helper only when the dependent props change.

Measuring the difference

The profiler now shows much better results: rendering NewsFeed is now down from +50ms to 5ms!

Better renders screenshot

PureComponent may make your performance worse

As with any performance optimization, it's critical to measure the how changes impact performance.

PureComponent is not an optimization that can blindly be applied to all components in your application. It's good for components in a list with deep render trees, which was the case in this example. If you're using arrow functions as props, inline objects, or inline arrays as props with a PureComponent, both shouldComponentUpdate and render will always get called, because new instances of those props will get created each time! Measure the performance of your changes to be sure they are an improvement.

It may be perfectly fine for your team to use inline arrow functions on simple components, such as binding onClick handlers on button elements inside a loop. Prioritize readability of your code first, then measure and add performance optimizations where it makes sense.

Bonus experiment

Since the pattern of creating components just to bind callbacks to props is pretty common in our codebase, we wrote a helper for generating components with pre-bound functions. Check it out on our Github repo.

You can also use windowing libraries, such as react-virtualized to avoid rendering components that aren't in view.

Thanks to Ian Mundy, Patrick Nausha, and Auresa Nyctea for providing feedback on early drafts of this post.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.