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:
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:
<NewsFeedItem contents={item.contents}>
<VisibilitySensor itemId={item.id} onChange={this.handleVisibilityChange} />
</NewsFeedItem>
This compiles down to:
React.createElement(
NewsFeedItem,
{ 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() {
<NewsFeedItem
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!
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.
Cover photo from Unsplash: https://unsplash.com/photos/ot-I4_x-1cQ
Top comments (2)
Great article, practical advice and motivation to open up the react profiler :)
Very good article, thanks!