DEV Community

Cover image for Functions Are Killing Your React App's Performance
Corbin Crutchley
Corbin Crutchley

Posted on • Originally published at unicorn-utterances.com

Functions Are Killing Your React App's Performance

Functions are an integral part of all JavaScript applications, React apps included. While I've written about how peculiar their usage can be, thanks to the fact that all functions are values, they help split up the monotony of your codebase by splitting similar code into logical segments.

This knowledge that functions are values can assist you when working on improving your React apps' performance.

Let's look at some of the ways that functions often slow down React applications, why they do so, and how to combat them in our own apps.

In this adventure, we'll see how to:

  • Memoize return values with useMemo
  • Prevent re-renders due to function instability
  • Remove costly render functions with component extraction
  • Handle children functions performantly

Memoizing return values with useMemo

Let's say that we're building an ecommerce application and want to calculate the sum of all items in the cart:

const ShoppingCart = ({items}) => {
    const getCost = () => {
        return items.reduce((total, item) => {
            return total + item.price;
        }, 0);
    }

    return (
        <div>
            <h1>Shopping Cart</h1>
            <ul>
                {items.map(item => <li>{item.name}</li>)}
            </ul>
            <p>Total: ${getCost()}</p>
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

This should show all items and the total cost, but this may cause headaches when ShoppingCart re-renders.

After all, a React functional component is a normal function, after all, and will be run like any other, where getCost is recalculated on subsequent renders when you don't memoize the value.

This getCost function may not be overly expensive when there are only one or two items in the cart, but this can easily become a costly computation when there are 50 items or more in the cart.

The fix? Memoize the function call using useMemo so that it only re-runs when the items array changes:

const ShoppingCart = ({items}) => {
    const totalCost = useMemo(() => {
        return items.reduce((total, item) => {
            return total + item.price;
        }, 0);
    }, [items]);

    return (
        <div>
            <h1>Shopping Cart</h1>
            <ul>
                {items.map(item => <li>{item.name}</li>)}
            </ul>
            <p>Total: ${totalCost}</p>
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

# Function instability causes re-renders

Let's expand this shopping cart example by adding in the ability to add new items to the shopping cart.

import {useState, useMemo} from 'react';
import {v4 as uuid} from 'uuid';

const ShoppingItem = ({item, addToCart}) => {
  return (
    <div>
      <div>{item.name}</div>
      <div>{item.price}</div>
      <button onClick={() => addToCart(item)}>Add to cart</button>
    </div>
  )
}

const items = [
  { id: 1, name: 'Milk', price: 2.5 },
  { id: 2, name: 'Bread', price: 3.5 },
  { id: 3, name: 'Eggs', price: 4.5 },
  { id: 4, name: 'Cheese', price: 5.5 },
  { id: 5, name: 'Butter', price: 6.5 }
]

export default function App() {
  const [cart, setCart] = useState([])

  const addToCart = (item) => {
    setCart(v => [...v, {...item, id: uuid()}])
  }

  const totalCost = useMemo(() => {
    return cart.reduce((acc, item) => acc + item.price, 0)
  }, [cart]);

  return (
    <div style={{display: 'flex', flexDirection: 'row', flexWrap: 'nowrap'}}>
      <div style={{padding: '1rem'}}>
        <h1>Shopping Cart</h1>
        {items.map((item) => (
          <ShoppingItem key={item.id} item={item} addToCart={addToCart} />
        ))}
      </div>
      <div style={{padding: '1rem'}}>
        <h2>Cart</h2>
        <div>
          Total: ${totalCost}
        </div>
        <div>
          {cart.map((item) => (
            <div key={item.id}>{item.name}</div>
          ))}
        </div>
      </div>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

If I now click any of the items' Add to cart buttons, it will:

1) Trigger the addToCart function
2) Update the cart array using setCart
1) Generating a new UUIDv4 for the item in the cart
3) Cause the App component to re-render
4) Update the displayed items in the cart
5) Re-run the totalCost useMemo calculation

This is exactly what we'd expect to see in this application. However, if we open the React Developer Tools and inspect our Flame Chart, we'll see that all ShoppingItem components are re-rendering, despite none of the passed items changing.

 raw `ShoppingItem key=

The reason these components are re-rendering is that our addToCart property is changing.

That's not right! We're always passing the same addToCart function on each render!

While this may seem true at a cursory glance, we can check this with some additional logic:

// This is not good production code, but is used to demonstrate a function's reference changing
export default function App() {
  const [cart, setCart] = useState([])

  const addToCart = (item) => {
    setCart(v => [...v, {...item, id: uuid()}])
  }

  useLayoutEffect(() => {
    if (window.addToCart) {
      console.log("addToCart is the same as the last render?", window.addToCart === addToCart);
    }

    window.addToCart = addToCart;
  });

  // ...
}
Enter fullscreen mode Exit fullscreen mode

This code:

  • Sets up addToCart function inside of the App
  • Runs a layout effect on every render to:
    • Assign addToCart to window.addToCart
    • Checks if the old window.addToCart is the same as the new one

With this code, we would expect to see true if the function is not reassigned between renders. However, we instead see:

addToCart is the same as the last render? false

This is because, despite having the same name between renders, a new function reference is created for each component render.

Think of it this way: Under-the-hood, React calls each (functional) component as just that - a function.

Imagine we're React for a moment and have this component:

// This is not a real React component, but is a function we're using in place of a functional component
const component = ({items}) => {    
  const addToCart = (item) => {
    setCart(v => [...v, {...item, id: uuid()}])
  }

  return {addToCart};
}
Enter fullscreen mode Exit fullscreen mode

If we, acting as React, call this component multiple times:

// First "render"
const firstAddToCart = component().addToCart;
// Second "render"
const secondAddToCart = component().addToCart;

// `false`
console.log(firstAddToCart === secondAddToCart);
Enter fullscreen mode Exit fullscreen mode

We can see a bit more clearly why addToCart is not the same between renders; it's a new function defined inside of the scope of another function.

Create function stability with useCallback

So, if our ShoppingItem is re-rendering because our addToCart function is changing, how do we fix this?

Well, we know from the previous section that we can use useMemo to cache a function's return between component renders; what if used that here as well?

export default function App() {
  const [cart, setCart] = useState([])

  const addToCart = useMemo(() => {
    return (item) => {
      setCart(v => [...v, {...item, id: uuid()}])
    }
  }, []);

  // ...

  return (
    <div style={{display: 'flex', flexDirection: 'row', flexWrap: 'nowrap'}}>
      <div style={{padding: '1rem'}}>
        <h1>Shopping Cart</h1>
        {items.map((item) => (
          <ShoppingItem key={item.id} item={item} addToCart={addToCart} />
        ))}
      </div>
      {/* ... */}
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Here, we're telling React never to re-initialize the addToCart function by memoizing the logic inside of a useMemo.

We can validate this by looking at our flame chart in the React DevTools again:

App is the only component that re-renders thanks to

And re-checking the function reference stability using our window trick:

// ...

const addToCart = useMemo(() => {
  return (item) => {
    setCart(v => [...v, {...item, id: uuid()}])
  }
}, []);

useLayoutEffect(() => {
  if (window.addToCart) {
    console.log("addToCart is the same as the last render?", window.addToCart === addToCart);
  }

  window.addToCart = addToCart;
});

// ...
Enter fullscreen mode Exit fullscreen mode

addToCart is the same as the last render? true

This use-case of memoizing an inner function is so common that it even has a shortform helper called useCallback:

const addToCart = useMemo(() => {
  return (item) => {
    setCart(v => [...v, {...item, id: uuid()}])
  }
}, []);

// These two are equivilant to one another

const addToCart = useCallback((item) => {
  setCart(v => [...v, {...item, id: uuid()}])
}, []);
Enter fullscreen mode Exit fullscreen mode

Render functions are expensive

So, we've demonstrated earlier how functions like this:

<p>{someFn()}</p>
Enter fullscreen mode Exit fullscreen mode

Can often be bad for your UI's performance when someFn is expensive.

Knowing this, what do we think about the following code?

export default function App() {
  // ...

  const renderShoppingCart = () => {
    return <div style={{ padding: '1rem' }}>
      <h2>Cart</h2>
      <div>
        Total: ${totalCost}
      </div>
      <div>
        {cart.map((item) => (
          <div key={item.id}>{item.name}</div>
        ))}
      </div>
    </div>;
  }

  return (
    <div style={{ display: 'flex', flexDirection: 'row', flexWrap: 'nowrap' }}>
      <div style={{ padding: '1rem' }}>
        <h1>Shopping Cart</h1>
        {items.map((item) => (
          <ShoppingItem key={item.id} item={item} addToCart={addToCart} />
        ))}
      </div>
      {renderShoppingCart()}
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Here, we're defining a renderShoppingCart function inside of App and calling it inside of our return render statement.

At first glance, this seems bad because we're calling a function inside of our template. However, if we think about it more, we may come to the conclusion that this is not entirely dissimilar to what React is doing anyway.

After all, React must be running the div for each render anyways, right? ... Right?

Not quite.

Let's look at a more minimal version of the above:

const Comp = ({bool}) => {
    const renderContents = () => {
        return bool ? <div/> : <p/>
    }

    return <div>
        {renderContents()}
    </div>
}
Enter fullscreen mode Exit fullscreen mode

Now, let's take a step even further within the renderContents function:

return bool ? <div/> : <p/>
Enter fullscreen mode Exit fullscreen mode

Here, JSX might be transformed into the following:

return bool ? React.createElement('div') : React.createElement('p')
Enter fullscreen mode Exit fullscreen mode

After all, all JSX is transformed to these React.createElement function calls during your app's build step. This is because JSX is not standard JavaScript and needs to be transformed to the above in order to execute in your browser.

This JSX to React.createElement function call changes when you pass props or children:

<SomeComponent item={someItem}>
    <div>Hello</div>
</SomeComponent>
Enter fullscreen mode Exit fullscreen mode

Would be transformed to:

React.createElement(SomeComponent, {
    item: someItem
}, [
    React.createElement("div", {}, ["Hello"])
])
Enter fullscreen mode Exit fullscreen mode

Notice how the first argument is either a string or a component function, while the second argument is props to pass to said element. Finally, the third argument of createElement is the children to pass to the newly created element.

Knowing this, let's transform Comp from JSX to createElement function calls. Doing so changes:

const Comp = ({bool}) => {
    const renderContents = () => {
        return bool ? <div/> : <p/>
    }

    return <div>
        {renderContents()}
    </div>
}
Enter fullscreen mode Exit fullscreen mode

To:

const Comp = ({bool}) => {
    const renderContents = () => {
        return bool ? React.createElement('div') : React.createElement('p')
    }

    return React.createElement('div', {}, [
        renderContents()    
    ])
}
Enter fullscreen mode Exit fullscreen mode

With this transform applied, we can see that whenever Comp re-renders, it will re-execute the renderContents function, regardless of it needs to or not.

This might not seem like such a bad thing, until you realize that we're creating a brand new div or p tag on every render.

Were the renderContents function to have multiple elements inside, this would be extremely expensive to re-run, as it would destroy and recreate the entire subtree of renderContents every time. We can fact-check this by logging inside of the div render:

const LogAndDiv = () => {
    console.log("I am re-rendering");
    return React.createElement('div');
}

const Comp = ({bool}) => {
    const renderContents = () => {
        return bool ? React.createElement(LogAndDiv) : React.createElement('p')
    }

    return React.createElement('div', {}, [
        renderContents()    
    ])
}

export const App = () => React.createElement(Comp, {bool: true});
Enter fullscreen mode Exit fullscreen mode

And seeing that I am re-rendering occurs whenever Comp re-renders, without fail.

What can we do to fix this?

Re-use useCallback and useMemo to avoid render function re-initialization

If we think back on earlier sections of this article, we may think to think reach for a set of tools we're already familiar with: useMemo and useCallback. After all, if the issue is that renderContents is not providing a stable reference of values, we now know how to fix that.

Let's apply the two to our codebase to see if it fixes the problem:

const LogAndDiv = () => {
    console.log("I am re-rendering");
    return <div/>;
}

const Comp = ({bool}) => {
    // This is a suboptimal way of solving this problem
    const renderContents = useCallback(() => {
        return bool ? <LogAndDiv/> : <p/>
    }, [bool]);

    const renderedContents = useMemo(() => renderContents(), [renderContents]);

    return <div>
        {renderedContents}
    </div>
}
Enter fullscreen mode Exit fullscreen mode

Let's re-render this component annnnnnnnnnnd...

Success! It only renders LogAndDiv when bool changes to true, then never re-renders again.

But wait... Why is there a comment in the code sample above that says it's a suboptimal way of solving this problem?

Remove costly render functions with component extraction

The reason it's not ideal to use useMemo and useCallback to prevent render functions' rerenders is because:

1) It's challenging to debug, the stacktrace for these renderContents is harder to follow.
2) It creates longer components without the ability to portably move these sub-elements.
3) You're doing React's job without knowing it.

Like, if we think about what renderContents is doing for a moment, it's acting like a child component that shares the same lexical scoping as the parent. The benefit of doing so is that you don't need to pass any items to the child, but it comes at the cost of DX and performance.

Instead of this:

const LogAndDiv = () => {
    console.log("I am re-rendering");
    return <div/>;
}

const Comp = ({bool}) => {
    // This is a suboptimal way of solving this problem
    const renderContents = useCallback(() => {
        return bool ? <LogAndDiv/> : <p/>
    }, [bool]);

    const renderedContents = useMemo(() => renderContents(), [renderContents]);

    return <div>
        {renderedContents}
    </div>
}
Enter fullscreen mode Exit fullscreen mode

We should be writing this:

const LogAndDiv = () => {
    console.log("I am re-rendering");
    return <div/>;
}

const Contents = () => {
    return bool ? <LogAndDiv/> : <p/>
}

const Comp = ({bool}) => {
    return <div>
        <Contents bool={bool}/>
    </div>
}
Enter fullscreen mode Exit fullscreen mode

This will solve our performance problems without needing a useMemo or a useCallback.

How?

Well, let's again dive into how JSX transforms:

const LogAndDiv = () => {
    console.log("I am re-rendering");
    return React.createElement('div');
}

const Contents = () => {
    return bool ? React.createElement(LogAndDiv) : React.createElement('p')
}

const Comp = ({bool}) => {
    return React.createElement('div', {}, [
        React.createElement(Contents, {
            bool: bool
        })
    ]);
}
Enter fullscreen mode Exit fullscreen mode

Let's look closer at what React's createElement actually returns. Let's console.log it:

console.log(React.createElement('div'));
// We could also: `console.log(<div/>)`
Enter fullscreen mode Exit fullscreen mode

Doing so gives us an object:

// Some keys are omitted for readability
{
  "$$typeof": Symbol("react.element"),
  key: null,
  props: { },
  ref: null,
  type: "div",
}
Enter fullscreen mode Exit fullscreen mode

Notice this object does not contain any instructions on how to create the element itself. This is the responsibility of react-dom as the renderer. This is how projects like React Native can render to non-DOM targets using the same JSX as react.

This object is known as a "Fiber node", part of React's reconciler called "React Fiber".

Very broadly, React Fiber is a way of constructing a tree off of the passed elements from JSX/createElement. For web projects, this tree is a mirrored version of the DOM tree and is used to reconstruct the UI when a node re-renders. React then uses this tree, called a "Virtual DOM" or "VDOM", to intelligently figure out which nodes need to be re-rendered or not based off of the state and passed props.

This is true even for functional components. Let's call the following:

const Comp = () => {
  return <p>Comp</p>;
}

console.log(<Comp/>);
// We could also: `console.log(React.createElement(Comp))`
Enter fullscreen mode Exit fullscreen mode

This will log out:

{
  "$$typeof": Symbol("react.element"),
  key: null,
  props: {  },
  ref: null,
  type: function Comp(),
}
Enter fullscreen mode Exit fullscreen mode

Notice how type is still the function of Comp itself, not the returned div Fiber node. Because of this, React is able to prevent a re-render if Comp is not needed to be updated.

However, if we instead call the following code:

const Comp = () => {
  return <p>Comp</p>;
}

console.log(Comp());
Enter fullscreen mode Exit fullscreen mode

We now get the fiber node of the inner p tag:

{
  "$$typeof": Symbol("react.element"),
  key: null,
  props: { children: [ "Comp" ] },
  ref: null,
  type: "p",
}
Enter fullscreen mode Exit fullscreen mode

This is because React is no longer in control of calling Comp on your behalf and is always called when the parent component is rendered.

The solution, then? Never embed a component inside of a parent component. Instead, move the child component out of the scope of the parent and pass props.

For example, convert this:

export default function App() {
  // ...

  const renderShoppingCart = () => {
    return <div style={{ padding: '1rem' }}>
      <h2>Cart</h2>
      <div>
        Total: ${totalCost}
      </div>
      <div>
        {cart.map((item) => (
          <div key={item.id}>{item.name}</div>
        ))}
      </div>
    </div>;
  }

  return (
    <div style={{ display: 'flex', flexDirection: 'row', flexWrap: 'nowrap' }}>
      <div style={{ padding: '1rem' }}>
        <h1>Shopping Cart</h1>
        {items.map((item) => (
          <ShoppingItem key={item.id} item={item} addToCart={addToCart} />
        ))}
      </div>
      {renderShoppingCart()}
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

To this:

const ShoppingCart = ({cart, totalCost}) => {
  return <div style={{ padding: '1rem' }}>
    <h2>Cart</h2>
    <div>
      Total: ${totalCost}
    </div>
    <div>
      {cart.map((item) => (
        <div key={item.id}>{item.name}</div>
      ))}
    </div>
  </div>;
}

export default function App() {
  // ...

  return (
    <div style={{ display: 'flex', flexDirection: 'row', flexWrap: 'nowrap' }}>
      <div style={{ padding: '1rem' }}>
        <h1>Shopping Cart</h1>
        {items.map((item) => (
          <ShoppingItem key={item.id} item={item} addToCart={addToCart} />
        ))}
      </div>
      <ShoppingCart cart={cart} totalCost={totalCost} />
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Children functions are helpful

While seldom used, there are some instances where you may want to pass a value from a parent component down to a child.

Let's look at ShoppingCart once again:

const ShoppingCart = ({cart, totalCost}) => {
  return <div style={{ padding: '1rem' }}>
    <h2>Cart</h2>
    <div>
      Total: ${totalCost}
    </div>
    <div>
      {cart.map((item) => (
        <div key={item.id}>{item.name}</div>
      ))}
    </div>
  </div>;
}
Enter fullscreen mode Exit fullscreen mode

While this might work fine if all of your items use the same component to display, what happens if we want to customize each item displayed within ShoppingCart?

We could choose to pass the array of cart items as children:

const ShoppingCart = ({totalCost, children}) => {
  return <div style={{ padding: '1rem' }}>
    <h2>Cart</h2>
    <div>
      Total: ${totalCost}
    </div>
    <div>
      {children}
    </div>
  </div>;
}

const App = () => {
    // ...

    return (
        <ShoppingCart totalCost={totalCost}>
          {cart.map((item) => {
             if (item.type === "shoe") return <ShoeDisplay key={item.id} item={item}/>;
             if (item.type === "shirt") return <ShirtDisplay key={item.id} item={item}/>;
             return <DefaultDisplay key={item.id} item={item}/>;
          })}
        </ShoppingCart>
    )
}
Enter fullscreen mode Exit fullscreen mode

But what happens if we want to wrap each cart item inside of a wrapper element and have a custom display of item?

Well, what if I told you that you can pass a function a the option of children?

Let's look a small example of this:

const Comp = ({children}) => {
    return children(123);
}

const App = () => {
    return <Comp>
        {number => <p>{number}</p>}
    </Comp>

    // Alternatively, this can be rewritten as so:
    return <Comp children={number => <p>{number}</p>}/>
}
Enter fullscreen mode Exit fullscreen mode

Whoa.

Right?

OK, let's break this down a bit by removing JSX from the picture once again:

const Comp = ({children}) => {
    return children(123);
}

const App = () => {
    return React.createElement(
        // Element
        Comp,
        // Props
        {}, 
        // Children
        number => React.createElement('p', {}, [number])
    )
}
Enter fullscreen mode Exit fullscreen mode

Here, we can see clearly how the number function is being passed to Comp's children property. This function then returns its own createElement call, which is used as the returned JSX to be rendered in Comp.

Using children functions in production

Now that we've seen how children functions work under-the-hood, let's refactor the following component to use them:

const ShoppingCart = ({totalCost, children}) => {
  return <div style={{ padding: '1rem' }}>
    <h2>Cart</h2>
    <div>
      Total: ${totalCost}
    </div>
    <div>
      {children}
    </div>
  </div>;
}

const App = () => {
    // ...

    return (
        <ShoppingCart totalCost={totalCost}>
          {cart.map((item) => {
             if (item.type === "shoe") return <ShoeDisplay key={item.id} item={item}/>;
             if (item.type === "shirt") return <ShirtDisplay key={item.id} item={item}/>;
             return <DefaultDisplay key={item.id} item={item}/>;
          })}
        </ShoppingCart>
    )
}
Enter fullscreen mode Exit fullscreen mode

Now that we have a baseline, let's look at how we can use this in production:

const ShoppingCart = ({totalCost, cart, children}) => {
  return <div style={{ padding: '1rem' }}>
    <h2>Cart</h2>
    <div>
      Total: ${totalCost}
    </div>
    <div>
      {cart.map((item) => (
        <Fragment key={item.id}>
          {children(item)}
        </Fragment>
      ))}
    </div>
  </div>;
}

const App = () => {
    // ...

    return (
        <ShoppingCart cart={cart} totalCost={totalCost}>
          {(item) => {
            if (item.type === "shoe") return <ShoeDisplay item={item}/>;
            if (item.type === "shirt") return <ShirtDisplay item={item}/>;
            return <DefaultDisplay item={item}/>;
          }}
        </ShoppingCart>
    )
}
Enter fullscreen mode Exit fullscreen mode

The problem with child functions

Let's run a modified version of the above code through our profiler once again. This time, however, we'll add a method of updating state entirely unrelated to ShoppingCart to make sure that we're not needlessly re-rendering each item on render:

import { useState, useCallback, Fragment } from 'react';

const items = [
  { id: 1, name: 'Milk', price: 2.5 },
  { id: 2, name: 'Bread', price: 3.5 },
  { id: 3, name: 'Eggs', price: 4.5 },
  { id: 4, name: 'Cheese', price: 5.5 },
  { id: 5, name: 'Butter', price: 6.5 }
]

const ShoppingCart = ({ children }) => {
  return <div>
    <h2>Cart</h2>
    <div>
      {items.map((item) => (
        <Fragment key={item.id}>
          {children(item)}
        </Fragment>
      ))}
    </div>
  </div>;
}

export default function App() {
  const [count, setCount] = useState(0)

  // Meant to demonstrate that nothing but `count` should re-render
  const addOne = useCallback(() => {
    setCount(v => v+1);
  }, []);

  return (
    <div>
      <p>{count}</p>
      <button onClick={addOne}>Add one</button>
      <ShoppingCart>
        {(item) => {
          if (item.type === "shoe") return <ShoeDisplay item={item} />;
          if (item.type === "shirt") return <ShirtDisplay item={item} />;
          return <DefaultDisplay item={item} />;
        }}
      </ShoppingCart>
    </div>
  )
}

function ShoeDisplay({ item }) {
  return <p>{item.name}</p>
}
function ShirtDisplay({ item }) {
  return <p>{item.name}</p>
}
function DefaultDisplay({ item }) {
  return <p>{item.name}</p>
}
Enter fullscreen mode Exit fullscreen mode

Unfortunately, when we do this, we can see that ShoppingCart re-renders anyway:

Why did this render?

This happens because, as the message in the profiler says, the function reference of children changes on every render; causing it to act as if a property was changed that required a re-render.

The fix to this is the same as the fix to any other function changing reference: useCallback.

export default function App() {
  const [count, setCount] = useState(0)

  const addOne = useCallback(() => {
    setCount(v => v+1);
  }, []);

  const shoppingCartChildMap = useCallback((item) => {
    if (item.type === "shoe") return <ShoeDisplay item={item} />;
    if (item.type === "shirt") return <ShirtDisplay item={item} />;
    return <DefaultDisplay item={item} />;
  }, []);

  return (
    <div>
      <p>{count}</p>
      <button onClick={addOne}>Add one</button>
      <ShoppingCart>
        {shoppingCartChildMap}
      </ShoppingCart>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

Hopefully, this has been a helpful insight into the world of React application performance improvements.

Be cautious when using useCallback and useMemo, however; Not all instances of your codebase need them and they may harm performance in some edge cases rather than help.

Interested in learning more about performance improvements for your React applications? Take a look at my upcoming book series, "The Framework Field Guide", which not only teaches React internals but walks you through React, Angular, and Vue code all at the same time, teaching you all three at once.

Top comments (23)

Collapse
 
chasm profile image
Charles F. Munat

Sorry to disagree but it is not functions that are slowing down your application.

It's REACT. And in particular re-rendering.

To blame functions (or the poor use of them in React) is pretty ridiculous, but it fits with my claim that most "front-end devs" are really "React devs" and see all of HTML, CSS, and JS through the prism of the React library/framework. How sad. They are so blinded by their abstraction layer that they can no longer see the forest for the trees.

Dump the React and the problem goes away. The DOM is actually quite fast. React is much slower.

Collapse
 
crutchcorn profile image
Corbin Crutchley • Edited

So what you're saying is that code without React, like this:

document.head.innerHTML = "";
document.body.innerHTML =  `<div id="root"></div>`

/**
 * Let's build this
 * <div id="root">
 *   <ul>
 *      <li>Item</li>
 *   </ul>
 *   <button>Add item</button>
 * </div>
 */

const root = document.querySelector("#root");

const addButton = document.createElement("button");
addButton.id = "addButton";
addButton.innerText = 'Add item';
root.append(addButton);

const listRoot = document.createElement("ul");
listRoot.id = "listRoot";
root.append(listRoot);


let count = 1;

function renderList() {
    const newChildren = [];
    for (let i = count; i > 0; i--) {
        const li = document.createElement('li');
        li.innerText = `Item ${i}`;
        newChildren.push(li)
    }
    listRoot.replaceChildren(...newChildren);
}

// For simplicity, don't cleanup - browser should WeakRef it away anyway
addButton.addEventListener("click", () => {
    count++;
    renderList();
})

renderList();
Enter fullscreen mode Exit fullscreen mode

Should be faster than this React code?

document.head.innerHTML = ``;
document.body.innerHTML =  ``

let reactReady = false;
let reactDOMReady = false;

const reactProd = document.createElement("script");
reactProd.onload = () => {
    reactReady = true;
    readyToRender();
}
reactProd.crossOrigin = "true";
reactProd.src = "https://unpkg.com/react@18/umd/react.production.min.js";
reactProd.type = 'text/javascript';

const reactDOMProd = document.createElement("script");
reactDOMProd.onload = () => {
    reactDOMReady = true;
    readyToRender();
}
reactDOMProd.crossOrigin = "true";
reactDOMProd.src = "https://unpkg.com/react-dom@18/umd/react-dom.production.min.js";
reactDOMProd.type = 'text/javascript';


document.head.append(reactProd, reactDOMProd);

const root = document.createElement('div');
root.id = "root";

document.body.append(root);

function readyToRender() {
    if (!reactReady || !reactDOMReady) return;

    function App() {
        const [count, setCount] = React.useState(1);

        const onClick = React.useCallback(() => {
            setCount(v => v + 1);
        }, []);

        const countArr = React.useMemo(() => {
            return Array.from({length: count}, (_, i) => count - i)
        }, [count]);

        return React.createElement(React.Fragment, null,
            React.createElement('button', {
                onClick,
            }, "Add item"),
            React.createElement("ul", null,                 
                countArr.map((str) => {
                    return React.createElement('li', {
                        key: str
                    }, "Item ", str)
                })
            ),
        )
    }

    ReactDOM.createRoot(root).render(
        React.createElement(App)
    );
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
chasm profile image
Charles F. Munat

Hmm. There are these languages called HTML and CSS? We can actually write them separately, rather than creating our entire page with JavaScript. But that's beside the point.

I am not going to waste time writing a lot of code to debunk this. I will simply make this point:

In the first instance, you are using JavaScript to create HTML on the fly.

In the second instance, you are using JAVASCRIPT to create HTML on the fly.

The only difference is that in the second instance, you've hidden some of the JavaScript behind the React library.

As React is just JavaScript (so far), and it works by generating HTML (and sometimes CSS) in the browser using JavaScript, then there is nothing you can do faster with React because I can simply write my JS to use the same tricks that React is using.

My guess here – and again, I'm not going to waste time on digging into it, but be my guest – is that React is creating the update in full as a string and then using innerHTML to update the DOM all at once, resulting in a single re-render (or something like that). But you've written your "JavaScript" cleverly to update the DOM over and over again. You're comparing apples and oranges (I didn't read your code closely, so correct me if I'm wrong). Kind of disingenuous. I'm guessing that you knew this in advance.

The React code is also more complex, longer, and harder to understand. But even if we say that React is faster than writing JS by hand, well, it is not as fast as SolidJS by a long shot. Or Svelte. So I still don't see the "benefit" of React. The only thing it has going for it is a huge "ecosystem" and a lot of cheerleaders. But that just encourages devs to use ever more dependencies on other people's code with all the security risk and bugginess that brings.

I'll stand by my initial comment. Where are your benchmarks by the way?

Thread Thread
 
crutchcorn profile image
Corbin Crutchley

Right here :)

This is the JS-only code:

Image description

This is the React code:

Image description

The code is about as apples-to-apples as they get. The only difference is that React's reconciler has saved us from re-rendering un-needed elements.

To do the same in JavaScript you'd have to write your own variant of a reconciler or change the way renders are made to be more complex from a DX perspective.

You're welcome to continue thinking that React is a bad tool, and there are valid criticisms of it.

The point I was making wasn't "React good" or even "No-React JS bad" it was "All languages/codebases have weaknesses; even without frameworks."

The fact you immediately took "This is how to improve your React's codebase's performance" to mean "Web developers are bad at their jobs" says more about you and your lack of ability to empathize with other developers (either that, or a lack of understanding of conceptual naunce) than it does React, React developers, or the web ecosystem as a whole.

Good day.

Thread Thread
 
chasm profile image
Charles F. Munat

Oh, I'm so sorry. I thought that the article had the title, "Functions Are Killing Your React App's Performance", which clearly states that functions = bad. I must have missed where it was entitled, "This is how to improve your React's codebase's performance". Did other readers see that title?

Most "readers" don't read articles. They read headlines. At best they scan articles. So what you say in your headline matters quite a bit. The original article has a clickbait headline. I reacted to that. I did not bother the read the article; neither do I intend to. It was a fail before the first paragraph.

So why didn't you just give it the right title in the first place? Hmm. And I don't remember saying anything about web developers being bad at their jobs, although now that you mention it, after decades in the business I have to say that most are indeed pretty mediocre. And lazy. Many, at least in enterprise, hardly do any work at all and most of that is merely configuration.

I was talking about devs in general. If you make it personal, well, that's your choice.

Why doesn't React modularize their code so that if all I want is a reconciler, then I can import only that? And if React is using a reconciler and the plain JS code is not, then how is that "apples to apples"? Let's have a 100km race. Only you are on a bicycle and I get a car. Fair, right? You'd bet big on yourself, no?

What is astonishing to me is how overly sensitive so many authors are. My original response was short and to the point. It was about the problem being in the React code, which you admit: "This is how to improve your React's codebase's performance".

I went on to claim that most "React devs" have become alienated from the HTML, CSS, and JS that actually runs the Web. I think there's plenty of evidence for this, but readers can make up their own minds.

But more importantly: I am no one important. You could have ignored me easily. Instead you've gone to a lot of trouble to try to crush me. Wow! One nobody says React is over and it's time for a pile on? Why so insecure? If React is so great, doesn't it speak for itself?

If I were a troll, I'd be high-fiving all my fellow trolls right now. It's almost a shame that I'm serious.

Oh, and news flash: everything that everyone says says more about themselves than anything else. Consider that when you write.

Collapse
 
chasm profile image
Charles F. Munat

Heh. I did go back out of curiosity (maybe I can learn something about vanilla JS!) to look at your "vanilla" example. But it made no sense to me. It seems to be doing a lot of unnecessary stuff. I thought we wanted to click a button and add numbered items to a list. It looks to me like you wrote the React version first, and then copied it into plain JS.

Isn't this much simpler?

<!DOCTYPE html>
<html>
  <head>
    <title>Tester</title>
  </head>
  <body>
    <main></main>
    <script>
      const addButton = document.createElement("BUTTON");
      const listRoot = document.createElement("UL");
      addButton.innerText = 'Add item';
      document.querySelector("MAIN").replaceChildren(addButton, listRoot);

      function appendItem () {
        const item = document.createElement("LI")
        item.innerText = `Item ${listRoot.children.length + 1}`
        listRoot.appendChild(item)
      }

      addButton.addEventListener("click", () => {
          appendItem();
      })

      appendItem();
    </script>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

If you really want the layout shift, change line 17 to listRoot.prepend(item).

Thread Thread
 
crutchcorn profile image
Corbin Crutchley • Edited

That's the entire point. My point was that the vanilla JS was not optimized. I intentionally made it worse than it could have been.

Once again; I absolutely understand hand-written JS can be faster than any framework. My point is "Any code can be written inefficiently".

I understand you're sold on the idea of telling newcomers to focus on the fundamentals. A noble effort.

What isn't noble is telling them that they're mediocre, should stop learning the tools they're already using, and that they "can't see the forest for the trees".

I get it, you don't like clickbait. Good on ya. But it gets people to read, is clearly tagged with #react and #reactnative and generally is an informational guide.

I'd implore you to, rather than fighting in the comments about how someone's work could be improved because you read the title and nothing else of the article; go write more of your own content like I see you've done prior.

Thread Thread
 
chasm profile image
Charles F. Munat

Not optimized? I wrote my version by starting from scratch with simple acceptance criteria and then not writing any code that wasn't necessary. No optimization required. With something more complex, I might have to revisit it once or twice. Seems like you're the one expecting people to write mediocre code.

Your article is enormously long. If you're going to lecture me on how important clickbait titles are to getting readers, you might want to do some research on article length. You could have made the same point much more succinctly and gotten more readers.

But I actually like your article the way it is. You get into reconciliation and React Fiber and trees and all sorts of deep computer science-y stuff (losing most of your readers, BTW). Why do you need all that React power?

I have a site with one page that has maybe 1000+ DOM objects on it, and Lighthouse complains that it is too big. How big are most pages? Why do they need all this complexity? What is it that we're trying to do that requires layer after layer of abstraction? People spend years learning not how to build web applications, but how to use React. That's crazy.

And that was my point.

As for writing, I do plenty – in the comments. Why on Earth would I write articles and have to compete with clickbait titles like yours when there is a ready-made audience pre-selected by you? It is much more efficient to piggyback on your work, and I don't see how it diminishes your work at all. After all, your article stands or falls on its own merits. If anything, I am driving more traffic to your pages. You should be thanking me, not imploring me to go away.

Thread Thread
 
crutchcorn profile image
Corbin Crutchley

I appreciate your feedback about the article.

I know it could have been shorter and I likely could have turned it into three distinct articles. Was considering doing so, but felt they cross-references themselves too often to make sense.

Plus, I have real world data from my time writing professional dev content that shows that despite higher bounce rate longer content has more average time spent on page and higher site/cross-content retention.

Collapse
 
brense profile image
Rense Bakker

Great article, every React dev should read it! 🔥

Collapse
 
crutchcorn profile image
Corbin Crutchley

Thanks so much for the kind words!

Collapse
 
efpage profile image
Eckehard

I watched this beautiful React Documentary recommended here, which is about the history of React. As far as I understood, React was just created to let people NOT think about implementation details. Your post sounds a bit like React lost it´s way...

Collapse
 
crutchcorn profile image
Corbin Crutchley

Letting people not focus about implementation detail is a nice ideal; but IMO practically impossible, regardless of which framework is where.

Even prior to Fiber, these issues were always present in React.

Further, even within frameworks that have nicer DX around performance, say, SolidJS with signals, there's still going to be some related issues that are caused by not using memos or whatnot.

This is all to say that as long as engineering exists, there will be nuance and context that's required to keep in mind to maintain performance. There will never be a one-size-fits-all performance improvement; it's tradeoffs all the way down.

Collapse
 
chasm profile image
Charles F. Munat

LOL.

So whether I'm right or wrong is inconsequential? Alert the media! Performance no longer matters.

It's astonishing to me that there is still anyone in tech who thinks that popularity = performance. React is popular because it's popular. Devs and companies choose React because other devs and companies are using React. It has a big ecosystem because it has been around for a long while.

But for that same reason it is legacy code, filled with bad ideas that just won't go away. It's not that the folks behind Svelte or Solid or any of the other options (hey, there is even plain JavaScript and the DOM) are smarter, they just came along later and learned from React's mistakes.

Now we can sail along on that React inertia, unwilling or afraid to try something new, or we can make efforts to improve by considering and even adopting new approaches. Even React does this, switching to hooks and now talking about signals.

But most importantly, writers on Dev.to should be ready to have their ideas and claims challenged. Is this about writing better code and helping newbies to get on the right path, or is it about stroking the egos of those who write here? I know, dumb question. Of course it's about ego.

I think the crappy attitude is yours. I made a comment about React and gave an explanation for why so many devs cling to it, namely, that it's all they really know these days. It was a call to get back to basics. You decided to take it personally, but I don't even know you. So that's on you.

Collapse
 
intermundos profile image
intermundos

This article is another perfect example why react a poorly engineered tool. All this useMemo and other internals should have never been exposed to developers.

Perfect solution for it - use solid.js and enjoy hassle free development.

Collapse
 
crutchcorn profile image
Corbin Crutchley

Even Solid has an equivalency to useMemo: solidjs.com/docs/latest#createcomp...

There is no silver bullet to development.

Collapse
 
intermundos profile image
intermundos

Yes, agree, no silver bullet. But if you dare to compare, react is a shit bullet and solid is wooden one :)

Now seriously, after 4 years of vue.js switching to working with react, makes me cry every day. It is just a bad piece of software. Will do my best to transition my company to Vue or at least solid.js.

Thread Thread
 
davedbase profile image
David Di Biase

The SolidJS community doesn't promote this panacea-like mentality. We're trying to build an engineering-first community, not a hype based one. I hope our community members/fans/supporters keep this in mind in the future when engaging with other communities. Let's be respectful to other approaches and ideas.

Thread Thread
 
crutchcorn profile image
Corbin Crutchley

Class act reply. Sincerely, thank you David 🙏

Collapse
 
codeofrelevancy profile image
Code of Relevancy

A great article..

 
chasm profile image
Charles F. Munat

Did you even read what I wrote? I'll let any other readers who make it this far decide for themselves. But then they were going to do that anyway.

Thread Thread
 
crutchcorn profile image
Corbin Crutchley

Here's the best part about all this; React isn't even my favorite among React, Angular, Vue, and Solid. In fact, it's by a wide margin my least favorite of these tools.

This is where you're mistaken - I have no particular marriage towards the tool but acknowledge its utility. One such example:

Web-tooled-based native apps? Hard to beat React Native. NativeScript is cool and all but lacks a similar-sized ecosystem, Something fairly key for many businesses.

Code-sharing with web and mobile using React Native, in particular, has been a huge boon for my team.

This impacts our apps; of course, it does. The bundle of our web applications is larger than I'd like them to be. But does that mean I throw the baby out with the bath water? No. We're a very small team in charge of a wide set of applications, and we're only able to manage as much work as we do, thanks to this code-sharing quality of React.

Performance for us isn't first-and-foremost but isn't unimportant either. It's a matter of balancing the business' needs and the user's needs.

Would it be possible to rewrite this into a smaller bundle with dedicated native apps, Solid.js, or some other set of tooling? Maybe, maybe.

But would it be feasible or make sense to the business to do so? In our case, absolutely not.

I'll reiterate my stance from another comment once again; React is not some greater good that will save your applications. It's even up for debate if React has earned the popularity it's garnered.

But for as long as it's used, there's no reason I shouldn't and can't talk about how to improve your existing React apps.

And that's the key: you're complaining that you're not able to criticize me and that my writing is only to "stroke the egos of those who write here". And yet, it's not me complaining about how other devs aren't using tools I approve of; it's you.

Newcomers are going to learn React whether or not you like it. While this is the case, we must teach them the concepts of the frameworks they're using. Not only does this improve their current applications, but it forces them to restructure their approach to development in general. The more devs we can get to think about how a reconciler works, the more devs will be likely to - as you put it - "see the forest for the trees".

I encourage and welcome you to criticize my writing - it's the only way I improve, and goodness knows I need it. But let's do that, then, shall we? Criticize my writing, not divert attention away from the topic at hand so that we can simply complain about other's tech stack when I'm actively trying to improve the experience for those using it through education.

Thread Thread
 
chasm profile image
Charles F. Munat

Sorry, but you're confused. I'm not responding to you. I honestly don't care at all what you think. We'll never meet. Nothing you do in your career will affect me in any way. Frankly, I don't care if you want to use scriptaculo.us or do everything in Cold Fusion. Have at it.

I am, however, a bit unhappy that you write clickbait titles (I note you've switched to defending yourself and don't mention the title at all), but only because they mislead newbies. Be honest about what you're saying. Maybe even start with "IF you are using React, here's something to look out for." At least that doesn't sound like React is the Web, which seems to be what a lot of React devs believe.

But as I say, I am not responding to you. My comments are actually directed at readers (more likely scanners) of your article, and my goal is precisely to "divert attention away from the topic at hand" so that readers consider that there are other ways to solve this problem (including giving up on React completely). And to correct anyone who, not reading the article itself, assumes that you're going back to recommending classes.

In short, I am addressing the underlying assumptions of your post – the part that readers might take for granted.

So it is the bystanders for whom I write, and my intent is to introduce other ideas. I am under no illusion that the great mass of readers will simply follow the herd. But maybe I can pick off a straggler or two.

I repeat what I said:

  1. These techniques are not about functions but rather about React and are only of value if you insist on using React. You can just as easily give React up or use another library.
  2. Most people who use React are, frankly, no longer "front-end devs" if they ever were. They know almost nothing about the DOM, HTML, CSS, or accessibility, having subcontracted those duties out to React or some library such as MUI.
  3. One option worth considering is to dump React in favor of a faster option, or even just manipulating the DOM directly (generally called vanilla JS).

Your reaction is essentially to set up straw men to prove I'm wrong (about which of the above three, exactly?), and the other commenter here is only concerned with taking all this personally, like I have any clue who they are or care. And then both of you keep doubling down. You just can't make this shit up.