React {children} performance win...

React {children} performance win...

I want to share with you a performance win using the popular react library that you might or might not know about.

So, all of us are using the children prop in React on a daily basis to achieve composition and reusability of the components, but, did you know that you can benefit from it to boost your react performance a ton with a very simple trick? let's see

So, let's get the most popular Counter example.

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <button onClick={() => setCount((count) => count + 1)}>+</button>
      <span>count is {count}</span>
      <button onClick={() => setCount((count) => count - 1)}>-</button>
    </div>
  );
}

function App() {
  return <Counter />;
}        

And, the output is:

a screen shot showing the result from the previous code which is a simple counter.

hint: here I'm using Vite and this is just the default styling, nothing special here.

Next, let's imagine we have a very slow component that performs an expensive calculation or renders a huge list like the following one.

function HugeList() {
  const items = Array.from({ length: 20000 }, () => "Item");

  return (
    <ul>
      {items.map((item, index) => (
        <li key={index}>{`${item} ${index}`}</li>
      ))}
    </ul>
  );
}        

you should adjust this 20000 to a number that your machine can handle, we shouldn't do this anyway but I'm just simulating a kind of expensive operation.

and now add this HugeList component to the Counter component.

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <button onClick={() => setCount((count) => count + 1)}>+</button>
      <span>count is {count}</span>
      <button onClick={() => setCount((count) => count - 1)}>-</button>
      <HugeList />
    </div>
  );
}        

now, what happens is, when you try to increase or decrease the count you will notice that it takes some time to reflect and render the newly updated value.

and this is of course because every time you try to change the counter value it causes the component to rerender and therefore the HugeList component to also rerender as it's a child component of the Counter.

So, now you might already think about using the react memo on the HugeList component but I want to show a different way with the children prop and explains why is this happening.

At this point let's have a look at the profiler and see what happens when changing the Counter value.

No alt text provided for this image

here you can notice that the HugeList component will rerender with every change that happens in the Counter component, and this is a normal react behavior.

So, to fix this you can memo the HugeList component, and the code will be something like

const HugeList = memo(function HugeList() {
  const items = Array.from({ length: 40000 }, () => "Item");

  return (
    <ul>
      {items.map((item, index) => (
        <li key={index}>{`${item} ${index}`}</li>
      ))}
    </ul>
  );
});

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <button onClick={() => setCount((count) => count + 1)}>+</button>
      <span>count is {count}</span>
      <button onClick={() => setCount((count) => count - 1)}>-</button>
      <HugeList />
    </div>
  );
}

function App() {
  return <Counter />;
}
        

so here we wrap a react component in the memo function and it will return a memoized version of that component that will skip rerenders as long as the props of this component didn't change over rerenders. you can read more about the react memo here.

Also, memoization isn't a thing specific to react, it's a concept in software engineering that is an optimization technique used primarily to speed up computer programs by storing the results of expensive function calls and returning the cached result when the same inputs occur again.

and we can still prove this from the profiler again.

No alt text provided for this image

now you can notice that the HugeList component didn't rerender again, also the app is responsive again with no delays happening because of the wasted and the expensive operation happens with the HugeList every time something happens with its parent (the Counter component) - again React rules.

Okay, great, but we wanted to prove that you can use the children prop to achieve almost the same thing!

so, let's write the code and then discuss it and analyze it with the profiler.

function HugeList() {
  const items = Array.from({ length: 40000 }, () => "Item");

  return (
    <ul>
      {items.map((item, index) => (
        <li key={index}>{`${item} ${index}`}</li>
      ))}
    </ul>
  );
}

function Counter({ children }: { children: React.ReactNode }) {
  const [count, setCount] = useState(0);

  return (
    <div>
      <button onClick={() => setCount((count) => count + 1)}>+</button>
      <span>count is {count}</span>
      <button onClick={() => setCount((count) => count - 1)}>-</button>
      {children}
    </div>
  );
}


function App() {
  return (
    <Counter>
      <HugeList />
    </Counter>
  );
}
        

and the profiler result will be

No alt text provided for this image

the profiler again tells us that the HugeList doesn't re-render anymore.

So, here we are accepting the HugeList as a child for the Counter component. but what does this have to do with not rerender the HugeList?

Children are not children, parents are not parents, memoization doesn’t work as it should, life is meaningless, re-renders control our life and nothing can stop them. the article.

let's break down what happened before to compare with the children hack.

  1. React renders the App component.
  2. The Counter component renders and renders its children which is the HugeList component.
  3. The user decides to change the counter (increment or decrement).
  4. this causes a state change which causes a component re-render (Counter component).
  5. the Counter component has children (HugeList) that get re-rendered in response to the parent re-render.

so, what happens now is the same thing except the fact that the HugeList component is rendered by the App component, so.

  1. React renders the App component.
  2. The App component now renders the Counter and the HugeList component.
  3. The App component doesn't maintain any state as of now so it will never re-render, and then the HugeList will never re-render.
  4. No matter what happens with the Counter component will not affect the HugeList component.

as of now, you might have some questions as I did when I knew about this. and I found this great article going in-depth in explaining how children prop works exactly in react. I will add some of the questions here to motivate you to read the article and adjust them to our example

Q1: Isn't the HugeList component still a child of the Counter component? we are literally rendering it as such.

Q2: what will happen if I passed children as a render function to share some data, so something like {children({ data })}

spoiler: it will re-render, but read the article to know why.

Q3: some other thoughts about combining this with react memo, useMemo, and useCallback hooks. it is great and worth every minute.

References:

  1. React components composition: how to get it right.
  2. The mystery of React Element, children, parents and re-renders.

Please let me know if you enjoyed it and what can be improved.

Rokaya Mohamed , CMA .

Senior Financial analyst

1 年

Great effort????keep going????

Karim Shalapy

Front-end Developer at Tangent

1 年

Great read, Eslam!! ??? ????? ?????

要查看或添加评论,请登录

社区洞察

其他会员也浏览了