Two mistakes in React.js we keep making over and over again

Yaroslav Serhieiev
Wix Engineering
Published in
21 min readFeb 20, 2019

Recently I was given a privilege to spend a whole two weeks, freestyle, on hunting down performance issues in a large-scale React Native app. There were a few good starting points that the QA team had suggested — some of the screens had a disturbingly slow reaction to such simple actions as tapping on a checkbox or typing a text into an input.

As my investigation went on, a bunch of seemingly isolated cases started resembling rather a deep rabbit hole. While it all started from a struggle with inefficient practices in user-facing components, I ended up with a discovery of similar shortcomings in React Native’s codebase itself:

Down the rabbit hole, towards React Native repo

Through this story, I want to share with you a few lessons that, in my opinion, are half-learned by many of us — the root cause of why we keep rubbing React’s fur the wrong way.

The Curious Case of Country Selector

I ask myself, if, in the beginning, I were to answer what can possibly go wrong with performance on the country selector screen below, could I assess how many potential points of failure I was up to discover? With all due skepticism, the reality has surpassed my wildest expectations.

Country Selector Demo

Basically, in this multi-selection list, we need to display all countries of the world, plus provide a “select all” checkbox and a quick search by name. At first sight, nothing special. So, what could be happening in an ordinary Redux app as you tap on a country item checkbox?

Scenario — tap on a checkbox

Here is a rough outline of how I’d personally expect the data flow to occur in this case.

My expectation for the data flow in “tap on country” scenario

Eight or so steps, a piece of cake for the modern hardware… but here is what I saw in reality on a very similar screen:

When React gets mistreated

Two eternities later, I even began to doubt if I ever made that tap on “South Korea” — all together, 7 seconds passed before I could see that white-on-green checkmark appearing. No doubt, something had to be done about it.

It had been a long time since my last dive into performance troubleshooting in React Native, so to refresh the memory and to make sure I wouldn’t overlook new insights or tools that had emerged meanwhile in React ecosystem, I put on replay a relevant presentation by Jean-Remi Beadouin I had a pleasure to see first-hand last November on React Day Berlin 2018.

Jean-Remi Beaudoin — Troubleshooting a slow app, practical guide to great React Native performance

So far, the most important slide to me was this one, the listing of relevant tools for in-app performance troubleshooting:

Below is a direct link to his presentation and this particular slide:

Without hesitation, I installed why-did-you-update npm package and monkey-patched the entry point to the app:

Pay attention at those two tweaks I did above to avoid issues with React’s YellowBox: the first — to disable it, and the second — to exclude it from why-did-you-update console logs. Otherwise, I would have run into the loop when a YellowBox would be shown to display a warning about an issue inside YellowBox — guess when the loop ends? Hint: never, at least in my specific React Native app, though your mileage may vary.

Mistake #1: 100 updates = 1 useful + 99 useless

Moving on, this is what I saw in the Console tab after cleaning all logs and tapping once on that list item:

why-did-you-update warnings, thousands of them!

You may notice that each wastefully updated component prints 2 warnings (one for state, and one for props), so I had an amused smile when I realized that here we have 3430 extra renders for one innocent tap.

The curiosity drove me to immediately try out the second recommendation on the list — Chrome debugger’s flame chart. In itself, visualizing the ongoing activity in React Native components turned out to be a dead simple task, done in four steps. Try this out if you haven’t already!

The picture I had discovered, once again confirmed the scale of futile activity happening behind the curtains:

So, why did that happen? I mean, only a handful of nodes were meant to be updated, and, speculatively, I can even tell which ones.

If we adhere to the immutability principle, and that is the premise behind the further reasoning, we know that instead of mutating an object or an array on a change, we should create a new instance of the latter. By that, we don’t lose its previous state irrevocably and yet ensure that strict equality check (===) returns false. To be on the safe side, I decided to supplement this paragraph with a code snippet as an illustration:

Keeping that in mind, let’s go over the chain of updates, we would typically expect when, for example, your reducers had already processed a tap on “Aland Islands.”

Component updates as we expect them to happen due to the changes in props (marked as red)
  • first of all, the connected CountrySelector component should have a force update from Redux’s Connect HOC (if it is not clear why, see its simplified version on Gist),
  • the underlying stateless template-like CountrySelector should update due to the changed property countries (as an array, it is expected to have a new reference since one of the items in that array has been modified),
  • FlatList should update because its data prop is different (for the same reason as countries),
  • only 1 out of about 200 underlying CellRenderer-s should update (not shown on the diagram, they are private components of VirtualizedList, the underlying component behind FlatList) — that’s easy to explain, because the rest of CellRenderer-s should receive array elements identical by reference, as none of them has been changed except for the array item associated with “Aland Islands”,
  • the ListItem with “Aland Islands” should update due to the new value of prop checked (=true) and a new reference to its country data item (as for the latter, it is supposed to be passed into always identical onChecked callback every time you tap on the checkbox),
  • and the CheckBox inside that ListItem should update because its value has explicitly been changed (from false to true).

In all fairness, I might have omitted a couple of props, but that’s not the point. I mean that, in total, maybe 6, maybe 10, maybe 15 components have to be updated — that, of course, depends on the nesting of components and other implementation details — but that’s not a magnitude of thousands we would expect here.

Before we continue, let’s revisit the diagram of React component lifecycle methods to understand my reasoning better. Although below is an updated version (for React 16.4 and later), the points made further are relevant to an equal extent to the older React versions.

Image source: https://blog.bitsrc.io/understanding-react-v16-4-new-component-lifecycle-methods-fa7b224efd7d

We know that React team has been encouraging the use of pure components (where it makes sense) for quite a while, and there’s no need to prove the obvious — update operations are apt to be long enough to stop us from being careless, especially when they involve some good 20–50 nodes. Moreover, that country selector, broken performance-wise, has vividly demonstrated what is going to happen if you lose control over updates in a big hierarchy of elements.

A modest but a crucial part of update lifecycle

I can’t overstate the importance of this modest red cross in React’s update lifecycle, especially if we figuratively “zoom out,” to grasp the entire hierarchy of components on the examined screen.

Updates the way they should be

Of course, my sketch cannot include the entirety of those 3000+ components in the example. With it, I want to stress how many components we can stop from entering the heavier parts of the update lifecycle, just with proper tuning of shouldComponentUpdate mechanism.

On the legend, I mark the components that undergo the full update lifecycle with a shade of red, the ones that exit early on shouldComponentUpdate — with its less saturated shade, and the ones that are entirely out of the loop — with white.

If I were to draw those updates the way they happened on that unfortunate screen, I should have repainted the whole tree just with red, and this could happen any day in an ordinary React app.

Grim reality — updates, updates everywhere

The good news is that these performance issues while being common, are still easy enough to fix. After dealing with a few problematic screens, I have identified a few places where the culprits usually dwell, so I’ll share a few insights on how we can fix such places and prevent their emergence.

A cut of Redux data flow under the examination

Let’s start in reverse order, from 4 to 1.

1.4. Your subcomponent is not actually pure

It won’t hurt to briefly revisit what a pure component is, especially since it takes a few lines in JavaScript to express the idea:

In other words, a pure component is the one that updates only when its props or state change, and, while you don’t necessarily want such limited behavior in all scenarios, it still is a typical case in a React app, and usually it’s an acceptable limitation for your average component.

Consider an example:

If Ownee inherits from a plain React.Component class, that means whenever its Owner decides to render, the Owner is going to render its Ownee together with all respective OwneeOfOwnee-s of the latter (albeit, they are painted differently on the diagram to emphasize that they, however, might stop updating if their shouldComponentUpdate returns false).

This behavior is not inherently bad, but at times it may decrease the update performance if you have many components to render inside Ownee.

Most likely, the things above were something everyone already knew about Component and PureComponent, but here’s something that caught me completely by surprise — that is, React’s stateless components.

I must confess, until a month ago, for some reason, I was convinced that a stateless component, generally speaking, behaves the same way as any PureComponent would. I mean, it is required to be a pure function, so at some point of time, I generalized that everything about a stateless component must be pure, including the behavior of shouldComponentUpdate.

However, that was a misconception, and I whistled upon my discovery that the opposite is true — a stateless component behaves exactly like a good old React.Component — whenever an owner renders, the ownee and ownees of the ownee render as well.

So, if a flame chart shows that a heavy stateless component gets often updated on non-relevant changes, what remedies are available then?

  • The most straightforward choice is to refactor your stateless component to a class derived from React.PureComponent (see the diff below):
  • To obtain more control over its update conditions, you can derive from a plain React.Component class and implement your custom shouldComponentUpdate method (see the relevant docs):
  • Alternatively, you can leverage the high-order React.memo component, introduced in React 16.6, and keep your stateless component the way it is, provided you wrap it with memo:
  • For an extra degree of control, you can pass a custom props equality comparer as a second argument to React.memo, as shown below:

It should further be noted that:

  1. The recipe is not intended to be exhaustive, as not always you can make a component pure just by changing three lines of code. Some components may need a total rewrite lest you break their logic by brutal putting PureComponent onto them, and at the end of the day, not all components need to be pure. That brings us to the second remark.
  2. Adding PureComponent-s recklessly here and there is apt to have the opposite effect on your performance. It seems like there is an apocryphal story about Airbnb going too far with using PureComponent, to an extent when it started hurting the performance.
https://github.com/yannickcr/eslint-plugin-react/issues/1667

So, as the adage credited to Paracelsus says, “The dose makes the poison.” In other words, do not optimize just for the sake of it. Every time you strive to optimize a piece of code, you need to measure various indicators before and after, to see if your changes actually improve and do not undermine the performance.

1.3. Your render function creates new references in child props components

Let’s call such props evernew. Oh, this is a wide-spread and very annoying pest in React apps. Before I make the point, let’s revisit the concept of shallow equality between objects.

While some minutiae may be different, it follows that if two arguments, a and b, are not identical by reference, but they both are objects with the same keys, and for all given keys the corresponding values are strictly equal between a and b, then a and b can be considered equal.

I suggest going over a few examples to make sure we are on the same page:

So, while primitive values in props are considered equal, neither objects nor arrays or functions are going to be. The more you pass anonymous objects, arrays, and functions to props of child components, the more their respective shouldComponentUpdate methods respond: “Yes, sure, let’s update!”. So the more your update tree gets “red,” as I previously illustrated on the update tree diagram, the more frustrated your users are going to be with increasing delays between taps and UI reaction.

Try to not “paint” all your update tree red, please!

However, this is not the ultimate truth. At times, a component update tree is too light to feel the consequences of deoptimization. Also, a component might be already in bad shape owing to the efforts of your predecessors — what is dead may never die, so to say — and another prop created along the way won’t really matter (except you care about keeping technical debt low 😉).

I have prepared a few katas on how to deal with this antipattern, and here is the first one:

As you see here, on every RegionManage render, we are luring its FlatList to join the computational party with a fresh-baked arrow function we pass to its renderItem prop. Performance penalty here increases as the more list items get involved.

The simplest way to stop tricking FlatList into rendering items (with always a quote-unquote drastically different renderItem function) is to convert RegionManage into a class and turn that arrow function into a class instance method as shown below:

What did we solve by that? With this change, whenever RegionManage gets an update not related to the items in that FlatList, it is going to leave it alone and concentrate on updating its more relevant siblings.

However, we have not solved the issue entirely, because on every list item render we still are passing an arrow function to onRemove property (see line 16). That means if the list gets updated, then all the items inside are going to be updated along for the ride.

Although the use of an arrow function is somewhat justified there since we need access both to this.props and to the item in the closure, it is not the end of the world yet. All we need to make it perform better is to teach the underlying TaxRegion to pass rule into the onRemove callback.

See how that immediately simplifies our code in RegionManage:

So now, the list items won’t be rendered in vain. As for the arrow function we left inside Region component (line 7 of Region.diff), I’d leave it as a homework for you, but in any case that can be solved by converting it to a class and adding a bound method, as I have demonstrated earlier on RegionManage component.

Before you say “Whoa! That guy tells us to give up functional programming, how dare you!”, I can reassure you there are ways to achieve the same results in functional style, although the code might end up looking more sophisticated. Here is a quick-n-dirty example:

That’s another perspective on how we can leave RegionManage stateless through extensive use of WeakMap-based memoizing. I don’t want to get us off the subject by explaining why the memoizeWeak trick works, but, to put it simply, this function in itself is similar to _.memoize, except that it can handle more than one argument correctly and it’s not expected to create memory leaks by design. As some of you might know, this class of solutions has a drawback — weak data structures cannot accept primitives as keys, so either you pass no numbers, strings, booleans, nulls, and undefined-s, or you resort to regular Map and Set for such memoizing, taking the risk of memory leaks.

I’m not ruling out other ways to keep props shallowly equal between renders. On the contrary, I leave it up to you to find your own ninja style. Moreover, the next case with a shared UI component, ListItem, bears testimony to the diversity of decisions you can make.

This particular ListItem class historically grew plenty of shaped props (complex objects), just like checkbox prop in the example below:

As I found out in the investigation process, the dealbreaker for CountrySelector performance with ListItem was that passing each time a new object for its checkbox prop triggered a full render inside. That was one of the major causes for why CountrySelector had been so slow.

Well, if we recall how PureComponent decides whether it should update, this behavior is justified — a new checkbox object is not going to be strictly equal to a new checkbox object, no surprise. Nevertheless, I found it extremely challenging to comply with that requirement of strict equality between checkbox objects in props — I had to ensure I generated identical objects whenever value and onValueChange were equal between renders.

On my every single attempt, either I found myself installing a weak memoization library, or building complicated selectors with reselect package. This way or another, it had been ending up looking so sophisticated that in the end, I gave up, as I didn’t believe I had to take such desperate measures just in order to satisfy that objectively limited mechanism of shouldComponentUpdate in PureComponent.

Thus, I changed tactics and replaced shouldComponentUpdate with my custom implementation that ditched strict equality checks for shaped properties like checkbox, button, and others in favor of shallow ones.

I hope that the code above conveys the idea of overriding equality checks for specific properties in a declarative way, but if not — you can examine the implementation of configureShouldComponentUpdate here on GitHub.

Therefore, passing an anonymous object into checkbox prop, after I relaxed checks in shouldComponentUpdate, ceased to be a performance crime (see line 5):

The only crime remaining (line 7) was that we still were accessing that item from a closure inside onValueChange arrow function to pass it into onSelectRow. Have we seen that somewhere before? Of course! Scroll up if you have already forgotten.

So all we need is to ensure that ListItem passes that item to checkbox.onValueChange on its own implementation level, and that’s it:

Summing up, you have enough degrees of freedom to work around evernew props antipattern — at the very least, you can:

  1. Extract arrow functions out of JSX, convert them either to static named functions or to bound class methods (if you need access to this keyword).
  2. Implement passing arguments to callback props on a child component level, so that its parent does not have to rely on closures in arrow functions. That way, you won’t need arrow functions anymore.
  3. If you feel it is too challenging to avoid evernew props in a specific case, consider overriding shouldComponentUpdate — at times it is going to be a cleaner solution. Keep in mind, however, that it might be a code smell, and you ought to rewrite your component creatively (with React Context, component composition or whatever may work for you).

Last but not least, there are ESLint rules you can leverage to prevent those places from happening in the first place, e.g., jsx-no-bind, jsx-no-new-object-as-prop, jsx-no-new-array-as-prop and some others (personal thanks to Ilya Ivanov for sharing that with me).

Interlude: when <FlatList> surprised me after all the optimizations

Eventually, after I had resolved most of the why-did-you-update warnings on that screen, the user experience significantly improved.

However, I had a gut feeling that not all is over because I could not eliminate those thin spikes under the “ScrollView [update]” bar. They appeared to be almost instantaneous, yet numerous updates of CellRenderer component used somewhere inside React Native’s VirtualizedList.

After one more investigation, I concluded that this was caused by passing an item-related data into the extraData prop of the FlatList.

Since the set of selectedCountriesIDs was tightly coupled with individual item actions like “tap to select or deselect,” therefore, on every checkbox toggle, it had been inevitably changing. What I didn’t realize back then, was the fact that when you pass extraData to the FlatList, the FlatList, in turn, passes that extraData to every rendered list item! Hence, every list item is doomed to update since the previous extraData cannot be equal to the next one by definition, as it carries selected IDs.

So, only the fact that the underlying template in renderItem had shouldComponentUpdate properly optimized, prevented React from further rendering downwards the children.

When I redefined the data model to stop depending on extraData as a source of information about specific items like on the snippet below…

… eventually, I got a chance to see this picture:

The holy grail — no extra spikes on the flame chart

You may see here that only one ListItem has updated during FlatList update lifecycle, which is a win. However, what makes the story even more interesting, is that it did not happen immediately after the rewrite!

At first, after all my effort to redesign data model, those familiar spikes under “ScrollView [update]” wouldn’t go away.

Not so easy to make those spikes go away, as it turned out

When I was out of the options, realizing that I had done everything possible and impossible, I again rummaged through why-did-you-update logs and noticed unexpected warnings coming directly from React Native classes. It occurred to me to google them up, just for kicks, and wow!

Source: https://github.com/facebook/react-native/issues/20174

What my surprise was when I saw an issue on Github with a complaint on a very similar arrow function to those we examined before, forgotten in the depths of React Native.

Fortunately, we’ve been collaborating with a guy who made a fix for that shortcoming and already have merged the fix into our fork of React Native. The official repository has that performance bug until now (Feb 2019). While its implications are not catastrophic, it slows down the responsiveness of lists in React Native quite a bit, given they approach hundreds of items and more.

The message is, no one is immune to mistakes, and to this deoptimization in particular, even React Native — at times.

So, constant vigilance, ladies and gentlemen!.. and may the force of ESLint, React Profiler and why-did-you-update be with you!

1.2. Your selector is malfunctioning

We are not over, by the way. There are more places where you can mess up with performance in React, and Redux apps, specifically.

It is common to use selectors with Redux, which can make your app more efficient in two ways: selectors save time on heavy calculations, and they automatically return identical references when a calculation is not required. No need to explain why identical references are nice to have — see section 1.2.

reselect leverages a light and concise memoization technique, and all its code fits into the snippet below:

While this example is perfectly fine:

This similar one is not:

I’m happy if you have already connected the dots, but if not — let’s look at the defaultMemoize implementation once more. After you finish, move on to the next paragraph.

Both selectors are singletons, so to say — consequently, every one of them has only one slot to keep information about its previous invocation. While this is not an issue for getActiveTodo, as the app state exists in a single copy, the problem with getTodoDisplayProps is that it is likely to be used for iteration over an array of todo items (and what else for? — a rhetorical question):

Since every time an argument is not strictly equal to the one from the last invocation, we are going to keep creating new object references on every getTodosForDisplay call.

Under these conditions, it gets more challenging to prevent update lifecycle reaching to a subcomponent with a repeater inside, even if a change in the current state was not related to state.todos at all. Besides, the getTodosForDisplay selector is unequivocally suboptimal, and for the sake of common sense, it would be better to write it as a plain function than to leave it as is.

However, if you wish to take your chances with using item-specific memoization on selectors level, you can try out, at the very list, the weak memoization trick I mentioned in section 1.2:

Once again, I want to emphasize that weak memoization is liable to bite a hand and inadvertently adds unnecessary complexity to application code. I admit that I had to use this technique in one place, but that’s mostly because I was afraid to suggest a more massive pull request with proper component restructuring, despite that the latter would exempt me from the need to use memoizeWeak. On the other hand, the risk to break something was lower, so I suggest to consider this approach as a temporary, symptomatic treatment, but it’s up to you.

1.1. Your reducer is more immutable than it should be

Imagine a potential implementation of the country selector screen in Redux. If a said country reducer was to process a presumed TOGGLE_COUNTRY_SELECTION action, it could have done that in two visually similar, but drastically different performance-wise ways.

The issue here slightly resonates with the selectors’ example above. A better reducer would optimize creating new references for the objects that have not been factually changed (see line 8 in countryReducer-good.js), while a worse implementation would create new object references all the time, and that would complicate further distinction between changed and unchanged props. It is not deadly, but it is likely to confuse some of the pure components inside your hierarchy and make them update for no good reason.

Mistake #2: 1 render creates 100 elements

Last but not least, try to avoid long render functions that create dozens of various JSX elements. It is essential in the light of partial bulk updates over UI elements. One of the prime examples is “Select all” functionality I have on the examined screen.

Scenario — tap on “Select all countries”

You may notice that time to select all countries approaches 1.5 seconds here — at least, that’s what I saw in iOS simulator on my laptop. So this is how the flame chart looks:

When we have a long flat render, we have to process updates (significant or not) in a big number of owned components, whether we like it or not.

Important notice: if you think that rewriting it into a PureComponent with numerous helper function calls makes it update any faster, you are very wrong! The structure stays the same, and it is still flat here:

That is, one ExpensiveComponent has six children and many-many ownees (concealed inside renderComponent11,…, renderComponent62). In terms of my analogy with red-pink-white update tree, any update here would look like the whole tree painted red:

You can probably see what suggestion is coming next —why don’t we repaint more nodes into white and pink?

To achieve that, we should arrange children as logically independent groups. For our case, a simple geographical grouping (e.g., left part, middle part, right part) may work out well enough. If we contain every group inside a pure component that knows better when to update, then on certain massive updates we can skip 50–70% of render work, because, often enough, only one part inside every list items needs to be updated, like a list item checkbox in the case with Select All scenario.

This small refactor decreased update time from 1.5s to 0.5s, which is a quick win and a low-hanging fruit. So, if you find yourselves in a similar situation, this trick might prove useful.

Summary

If UI slowly responds to simple actions like tapping on a checkbox in your React Native app, it’s a good time to arm yourself with why-did-you-update and Chrome debugger’s flame chart. Usually, the treatment is local in terms of codebase, recuperation is fast, and the result is noticeable and pleasing.

Avoid passing evernew props to children components. If you feel like you can enable ESLint performance rules on a permanent basis in your project, go for it — a linter never gets tired while you as a human definitely do.

The reason for the title — “Two mistakes in React.js we keep doing over and over again” — is that these days the issue is as valid as it was a few years ago. I hope I’ve managed to link together in one place many of the insights scattered over articles of past years and also share the first-hand experience from a different angle, using React Native app as an example.

With knowledge comes responsibility — make sure your fellows are aware of coding habits in React that compromise performance and don’t repeat the same mistakes twice, please.

Thanks for being with me throughout all the story and remember, it’s within our power to make the user experience more enjoyable!

Related articles

--

--