When should you NOT use React memo?

Viewed 61762

I've been playing around with React 16.6.0 recently and I love the idea of React Memo, but I've been unable to find anything regarding scenarios best suited to implement it.

The React docs (https://reactjs.org/docs/react-api.html#reactmemo) don't seem to suggest any implications from just throwing it on all of your functional components.

Because it does a shallow comparison to figure out if it needs to re-render, is there ever going to be a situation that negatively impacts performance?

A situation like this seems like an obvious choice for implementation:

// NameComponent.js
import React from "react";
const NameComponent = ({ name }) => <div>{name}</div>;
export default React.memo(NameComponent);

// CountComponent.js
import React from "react";
const CountComponent = ({ count }) => <div>{count}</div>;
export default CountComponent;

// App.js
import React from "react";
import NameComponent from "./NameComponent";
import CountComponent from "./CountComponent";

class App extends Component {
  state = {
    name: "Keith",
    count: 0
  };

  handleClick = e => {
    this.setState({ count: this.state.count + 1 });
  };

  render() {
    return (
      <div>
        <NameComponent name={this.state.name} />
        <CountComponent count={this.state.count} />
        <button onClick={this.handleClick}>Add Count</button>
      </div>
    );
  }
}

Because name will never change in this context, it makes sense to memoize.

But what about a situation where props change frequently?
What if I added another button that changed something else in the state and triggered a re-render, would it make sense to wrap CountComponent in memo, even though this component by design is meant to update frequently?

I guess my main question is as long as everything remains pure, is there ever a situation to not wrap a functional component with React Memo?

9 Answers

You should always use React.memo LITERALLY, as comparing the tree returned by the Component is always more expensive than comparing a pair of props properties

So don't listen to anyone and wrap ALL functional components in React.memo. React.memo was originally intended to be built into the core of functional components, but it is not used by default due to the loss of backward compatibility. (Since it compares the object superficially, and you MAYBE are using the nested properties of the sub-object in the component) =)

That's it, this is the ONLY REASON why React doesn't use memo Automatically. =)

In fact, they could make version 17.0.0, which would BREAK backward compatibility, and make React.memo the default, and make some kind of function to cancel this behavior, for example React.deepProps =)

Stop listening to theorists, guys =) The rule is simple:

If your component uses DEEP COMPARING PROPS then don't use memo, otherwise ALWAYS use it, comparing TWO OBJECTS is ALWAYS CHEAPER than calling React.createElement() and comparing two trees, creating FiberNodes, and so on.

Theorists talk about what they themselves do not know, they have not analyzed the react code, they do not understand FRP and they do not understand what they're advising =)

P.S. if your component is using children prop, React.memo will not work, because children prop always makes a new array. But It is better not to bother about this, and even such components should ALSO be wrapped in React.memo, since the computing resources are negligible.

All react components implement the shouldComponentUpdate() method. By default (components extending React.Component), this returns true, always. The change that memoizing a component (through React.memo for functional components or extending React.PureComponent for class components) introduces is an implementation of the shouldComponentUpdate() method - which does the shallow comparison of the state and props.

Looking at the documentation on the component lifecycle methods, shouldComponentUpdate() is always called before the render happens, this means that memoizing a component will include this additional shallow comparison on every update.

Taking this into consideration, memoizing a component does have performance effects, and the magnitude of these effects should be determined through profiling your application and determining whether it works better with or without the memoizing.

To answer your question, I don't think there's an explicit rule when you should or should not memoize components, however I think the same principle should be applied as when deciding whether or not you should override shouldComponentUpdate(): find performance issues through the suggested profiling tools and identify whether or not you need to optimise a component.

Is there ever going to be a situation that negatively impacts performance?

Yes. You may end up with worse performance, if all components are mindlessly wrapped by React.memo.

It is not needed in many cases. To give it a try with a performance critical component, do some measures first, add memoization and then measure again to see if added complexity was worth it.

What is the cost of React.memo?

A memoized component compares old with news props to decide, if to re-render - each render cycle.
A plain component does not care and just renders, after props/state change in a parent.

Take a look at React shallowEqual implementation, which is invoked in updateMemoComponent.

When NOT use React memo?

There are no hard rules. Things, that affect React.memo negatively:

  1. component often re-renders with props, that have changed anyway
  2. component is cheap to re-render
  3. comparison function is expensive to perform

Ad 1: In this case, React.memo cannot prevent a re-render, but had to do additional calculations.
Ad 2: Added comparison cost is not worth it for a "simple" component in terms of render, reconcile, DOM change and side-effect costs.
Ad 3: The more props, the more calculations. You also can pass in a more complex custom comparer.

When complement React.memo?

It only checks props, not context changes or state changes from inside. React.memo is also useless, if the memoized component has non-primitive children. useMemo can complement memo here, like:

// inside React.memo component
const ctxVal = useContext(MyContext); // context change normally trigger re-render
return useMemo(() => <Child />, [customDep]) // prevent re-render of children

The same question has an answer by markerikson on the React GitHub issue tracker. It got way more thumbs up than the answers here.

I would assume that the same general advice applies for React.memo as it does for shouldComponentUpdate and PureComponent: doing comparisons does have a small cost, and there's scenarios where a component would never memoize properly (especially if it makes use of props.children). So, don't just automatically wrap everything everywhere. See how your app behaves in production mode, use React's profiling builds and the DevTools profiler to see where bottlenecks are, and strategically use these tools to optimize parts of the component tree that will actually benefit from these optimizations.

The idea is to avoid using memoization, for data which has the possibility of changing very often. As stated in the blog, this also includes callbacks which depends on such types of data. For example functions such as

<Foo onClick={() => handle(visitCount)}/>

I really enjoyed this simplistic read. The examples are great. https://dmitripavlutin.com/use-react-memo-wisely/

I think the short answer is: React.memo does to Functional Components what React.PureComponent does to Class Components. In that sense, when you use memo it will evaluate if the props to that functional component have changed, if so, then it will execute the return of the fuction, otherwise it will not, avoiding re-render of the component.

import React, { memo } from 'react';

const Component = () => {
  debugger;
  return (
    <div>Hello World!</div>
  );
};

const MemoComponent = memo(() => {
  debugger;
  return (
    <div>Hello World!</div>
  );
});

If you use Component as child component of a container that updates, everytime the parent updates it will re-render (debugger will trigger everytime). In the other hand if you use MemoComponent it will not re-render (debugger will only trigger in the first render).

In this example that happens because the functional component doesn't have props, in case it had props it will happen only if the props change.

You should not overoptimize your code. You should optimize your code once you see a bottleneck. If you do not notice any slowdowns in your code, there is no reason to start optimizing ahead of time. Because these optimizations have a cost. Optimizations are not free. Optimization functions have cost on memory and storing those also takes time. React developers also work to make React more performant. React has tons of stuff under the hood to optimize react applications

It is best to use React.memo() when:

  • Components are rendered too frequently and slow down the application.

  • Component renders’ cost is high (When the loading time is more than 100ms).

  • Component keeps re-rendering for the same set of props.

Configurable embedded test case

I set up a test case with some parameters to measure the average render time, both for the memo and non-memo version of otherwise the same component. You can try it by running the code snippet below. It doesn't serve as a definitive answer, rather it helps demonstrate how different circumstances affect performance.

If you only change the "with memo" checkbox, it preserves the average times so you can compare both. Changing any other setting will reset the counters.

Note there's a small constant amount of time added by the input elements, but it's quite stable and doesn't prevent you from observing the relative impact of parameters. Though feel free to adjust this answer if you can make it more reliable/accurate.

For the memo version it also keeps track of the average time for hits and misses separately. A hit means the memo was able to skip a render.

You can set how frequently the memo component needs to run (default every 10 renders).

Finally, assuming the change detection algorithm would stop as soon as any difference is detected, I added a parameter to reduce the amount of props being changed. You can turn it up to 100% to make it change only a single prop, or 0% (default) to change all props. The last prop is always changed. So far this didn't seem to result in any measurable change. You can probably just ignore this setting.

Limitations

The props are simple equally sized strings, which probably makes the comparison operation easier than it is in real world cases.

Some timing info is written in an effect as to not "disturb" React too much. As a result some stale time info stays displayed until overwritten.

The very first render is considerably slower, you'll have to run some renders afterwards to cancel it out.

Accuracy

This is not meant as an accurate measurement, rather a way to compare the behavior of the 2 variants under otherwise similar circumstances.

While the App component has some expensive logic, the time is only measured after that. I used an effect for stopping the timer, as it's pretty much the first thing React will do after it's done rendering, should be close enough for this purpose.

My observations

The results of my own testing confirm the current top voted answer. The impact of the additional check is miniscule, even for absurdly large amounts of props, and/or props that are large strings.

If you use memo on a component that changes literally every time (change interval of 1), it can only be slower by a certain amount. So there's a turnover point where you start to gain from it.

However, I found that even if there's a 1 in 2 chance the component will need to render, memo came out favorably.

In fact the impact of using memo is so small that it's hard to observe even with many / big props.

The time avoided by skipping rendering, on the other hand, is measurably increased even for simple components.

You can try it for yourself with different parameters, the check is always much cheaper than the work it avoids. I did not find any configuration where not using memo was faster on average... Or did I?

memo is slower on extremely large strings*

*If your component doesn't use the large string.

Just as I was about to submit my answer, I tried again increasing the strings to 1 million characters. For the first time memo was struggling, whereas the component was not.

enter image description here

Even with only 1 in 10 "misses" it's clearly slower on average.

But if you're passing strings of that size as props you probably have more than 1 performance problem, and this likely won't be the biggest.

Also, in the rare case you do need to pass it, it would surely be used by the component. That would likely make it many times slower. Currently the test code doesn't do anything with these large values.

let renderTimes = 0;
let compRenderTimes = 0;

function randomString(length) {
    var result           = '';
    var characters       = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
    var charactersLength = characters.length;
    for ( var i = 0; i < length; i++ ) {
      result += characters.charAt(Math.floor(Math.random() *  charactersLength));
   }
   return result;
}

const {useState, useEffect, memo} = React;

let lastCompRenderStart;

function Comp (props) {
  compRenderTimes++;
  lastCompRenderStart = performance.now();
  
  // Expensive task.
  //console.log(props);
  /*Object.entries(props).forEach(([k,v]) => {
    if (`${k}${v}` === '4abc') {
      alert('What a coincidence!');
    }
  });*/
  
  useEffect(()=>{
    const duration = performance.now() - lastCompRenderStart;
    document.getElementById('lastCompRenderTime').innerHTML = duration.toFixed(2);
    document.getElementById('nCompRenders').innerHTML = compRenderTimes;
  });

  return <p className="test">Testing {Object.keys(props).length} props, last rendered {performance.now()}</p>;
};

const MemoComp = memo(Comp);

let lastProps = {};
let lastRenderStart;
let lastWasHit = false;
let currentTotal = 0;
let memoRenders = 0;
let memoHitTotal = 0;
let memoHitRenders = 0;
let memoMissTotal = 0;
let memoMissRenders = 0;
let nomemoRenders = 0;
let nomemoTotal = 0;

function App() {
  renderTimes++;
  const [,refresh] = useState();
  const [propAmount, setPropAmount] = useState(10);
  const [propSize, setPropSize] = useState(10);
  const [changeInterval, setChangeInterval] = useState(10);
  const [changedPropOffset, setChangedPropOffset] = useState(0);
  const [withMemo, setWithMemo] = useState(true);
  
  useEffect(()=>{
    
    renderTimes = 1;
    compRenderTimes = 1;
    currentTotal = 0;
    memoRenders = 0;
    memoHitTotal = 0;
    memoHitRenders = 0;
    memoMissTotal = 0;
    memoMissRenders = 0;
    nomemoRenders = 0;
    nomemoTotal = 0;
  }, [propAmount, propSize, changeInterval, changedPropOffset]);
  
  let props = {};
  lastWasHit = renderTimes !== 1 && renderTimes % changeInterval !== 0;
  if (lastWasHit) {
    // Reuse last props;
    props = lastProps;
  } else {
    // Generate random new values after offset.
    for (let i = 0; i < propAmount; i++) {
      if (!!lastProps[i] && (i * 100 / propAmount < changedPropOffset) && i < propAmount - 1) {
        props[i] = lastProps[i];
      } else {
        props[i] = randomString(propSize);
      }
    }
    lastProps = props;
  }

  useEffect(()=>{
    const duration = performance.now() - lastRenderStart;
    document.getElementById('lastRenderTime').innerHTML = duration.toFixed(2);
    
    if (!withMemo) {
      nomemoRenders++;
      nomemoTotal += duration;
      document.getElementById('no-memo-renders').innerHTML = nomemoRenders;
      document.getElementById('average-no-memo').innerHTML = (nomemoTotal / nomemoRenders).toFixed(2);

    } else {
      memoRenders++;
      currentTotal += duration;
      document.getElementById('memo-renders').innerHTML = memoRenders;
      document.getElementById('average').innerHTML = (currentTotal / memoRenders).toFixed(2);

      if (lastWasHit) {
        memoHitRenders++;
        memoHitTotal += duration;
        document.getElementById('average-memo-hit').innerHTML = (memoHitTotal / memoHitRenders).toFixed(2);
      } else {
        memoMissRenders++;
        document.getElementById('memo-renders-miss').innerHTML = memoMissRenders;

        memoMissTotal += duration;
        document.getElementById('average-memo-miss').innerHTML = (memoMissTotal / memoMissRenders).toFixed(2);
      }
    }
  });

  const ActualComp = withMemo ? MemoComp : Comp;
  
  // This should give us the time needed for rendering the rest.
  // I assume the settings inputs have has a more or less constant small impact on performance, at least while they're not being changed.
  lastRenderStart = performance.now();

  return <div>
    <button onClick={() => refresh(renderTimes)} title='Trigger a render of App component'>render</button>
    <input type='checkbox' onChange={event=>setWithMemo(!withMemo)} checked={withMemo}/>
    <label  onClick={event=>setWithMemo(!withMemo)}>
     with memo - 
    </label>
    - Prop amount: <input type='number' title='How many props are passed to memoed component' value={propAmount} onChange={event=>setPropAmount(event.target.value)}/>
    Prop size: <input type='number' title='How many random characters in prop value string?' value={propSize} onChange={event=>setPropSize(event.target.value)}/><br/>
    Change interval: <input type='number' title='Change memoized component props every X renders.' value={changeInterval} onChange={event=>setChangeInterval(event.target.value)}/>
    Changed prop offset <input type='number' min={0} max={100} step={10} title='Leave the first X percent of the props unchanged. To test if props comparison algorithm is affected by how fast it can find a difference. The last prop is always changed.' value={changedPropOffset} onChange={event=>setChangedPropOffset(event.target.value)}/>
    <ActualComp {...props} />
  </div>;
};


ReactDOM.render(<App/>, document.getElementById('root'));
#lastRenderTime {
  background: yellow;
}

#lastCompRenderTime {
  background: lightblue;
}

.test {
  background: lightgrey;
  border-radius: 4px;
}

td {
  border: 1px solid lightgrey;
  padding: 4px;
 }
 
 input[type=number] {
     max-width: 72px;
 }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>

<div id="root"></div>

<table>
<th>
  <td>N renders</td>
  <td>Average duration</td>
  <td>Memo (hit) duration</td>
  <td>Memo (miss) duration</td>
</th>
<tr>
<tr>
<td>No memo</td>
<td><span id="no-memo-renders"></span></td>
<td><span id="average-no-memo"></span></td>
</tr>


<tr>
<td>memo</td>
<td><span id="memo-renders"></span>, <span id="memo-renders-miss"></span> miss</td>
<td><span id="average"></span></td>
<td><span id="average-memo-hit"></span></td>
<td><span id="average-memo-miss"></span></td>
</tr>
</table>
=====
<table>
  <tr>
<td>Last rendered App</td>
<td><span id="lastRenderTime"></span></td>
</tr>
<tr>
<td>Component renders</td>
<td><span id="nCompRenders"></span></td>
</tr>
<tr>
<td>Last rendered Component</td>
<td><span id="lastCompRenderTime"></span></td>
</tr>
</table>

"Remember that the function passed to useMemo runs during rendering. Don’t do anything there that you wouldn’t normally do while rendering. For example, side effects belong in useEffect, not useMemo.

You may rely on useMemo as a performance optimization, not as a semantic guarantee. In the future, React may choose to “forget” some previously memoized values and recalculate them on next render, e.g. to free memory for offscreen components. Write your code so that it still works without useMemo — and then add it to optimize performance. (For rare cases when a value must never be recomputed, you can lazily initialize a ref.)"

https://reactjs.org/docs/hooks-faq.html#is-it-safe-to-omit-functions-from-the-list-of-dependencies

Related