React Component Render Optimization Techniques

Ref: https://blog.isquaredsoftware.com/presentations/2022-10-guide-react-rendering-behavior/?slideIndex=17&stepIndex=0

Ref: https://www.developerway.com/posts/react-re-renders-guide

=================

A component render is considered "wasted" if it returns the same requested elements as before. Normally, UI = f(props + state), so if props and state haven't changed, the UI output should be the same.

We can optimize performance by skipping component renders if props haven't changed. This also skips the entire subtree of that component.

  • Primary: wrap a component with the React.memo() higher-order component

    • Automatically checks for props changes with "shallow equality" comparison
    • Accepts a custom comparison callback as an option
  • Class components: shouldComponentUpdate or React.PureComponent

=================

// ❌ BAD!
// Antipattern: Creating components in render function
// This creates a new `ChildComponent` reference every time!
function ParentComponent() {
  function ChildComponent() {    
    return <div>Hi</div>
  }
  
  return <ChildComponent />
}

// ✅ GOOD
// This only creates one component type reference
function ChildComponent() {
  return <div>Hi</div>
}

function ParentComponent() {
  return <ChildComponent />
}

// ✅ GOOD
// Use props.children
function Parent({children}) {
  const [counter, setCounter] = useState(0)

  // Consistent `props.children` element reference as state changes
  return (
    <div>
      <button onClick={increment}>Increment</button>
      {children}
    </div>
  )
}

// later
return <Parent><SomeChild /></Parent>
  
// ✅ GOOD
// Use useMemo to save an element reference
function ComponentA({data}) {
  // Consistent `ChildComponent` element reference
  const memoizedChild = useMemo(() => {
    return <ChildComponent data={data} />;
  }, [data])

  return <div>{memoizedChild}</div>
}

=================

Antipattern: Creating components in render function

Creating components inside render function of another component is an anti-pattern that can be the biggest performance killer. On every re-render React will re-mount this component (i.e. destroy it and re-create it from scratch), which is going to be much slower than a normal re-render. On top of that, this will lead to such bugs as:

  • possible “flashes” of content during re-renders
  • state being reset in the component with every re-render
  • useEffect with no dependencies triggered on every re-render
  • if a component was focused, focus will be lost

Preventing re-renders with composition: moving state down

Preventing re-renders with composition: children as props

Preventing re-renders with composition: components as props

=================

Immutability and Rerendering

React.memo() does "shallow equality" checks like props.someValue !== prevProps.someValue, assuming immutable updates will change references. Hooks with dependencies (useMemo/useCallback/useEffect) work similarly. 

If you mutate, then someValue is the same reference, and those components will assume nothing has changed.

In addition: useState and useReducer expect new state references in order to re-render. If you pass in the same state reference, React will bail out of the update!

React, and the rest of the React ecosystem, assume immutable updates. Any time you mutate, you run the risk of bugs. Don't do it!!!