There are several factors that can affect the performance of React rendering. Certain mistakes can cause React to perform poorly by making it do unnecessary tasks. The following sections highlight common mistakes that can lead to suboptimal React rendering performance.

React components are re-rendered when their state or props change. This can cause unnecessary re-rendering, leading to performance problems.

Check out this article for more information on good practices and common mistakes: React Re-renders Guide.

#Object references in states/contexts

Avoid using object references directly in context values. Each time a (re-)render occurs, a new object reference is created. Even if the content of the object is the same, React considers it as a change of state/context. This forces all components that depend on that context to be rendered again.

If you need to use an object in your state or context, use the useMemo function to avoid unnecessary rerenders.

Do

Wrap object references for context values with useMemo

const MyComponent = ({ children }) => {
  const memoizedValue = useMemo<ILocalization>(() => {
    return {
      country,
      language,
    };
  }, [language, country]);

  return (
    <LocalizationContext.Provider value={memoizedValue}>
      {children}
    </LocalizationContext.Provider>
  );
};

Don't

A context value should never get an object reference directly. Each time a re-render occurs, a new object reference is created.

const MyComponent = ({ children }) => {
  return (
    <LocalizationContext.Provider
      value={{
        country,
        language,
      }}
    >
      {children}
    </LocalizationContext.Provider>
  );
};

#Function references in states/contexts

Context handlers are often defined inside the render functions of our components. This means that whenever the component re-renders, a new function reference is created for the handler. As a result, the context is updated, causing all consumers of the context to rerender as well.

To avoid this, you can use the useCallback or useMemo function from React.

Do

Memoizing the handler with useMemo or useCallback to prevent unnecessary re-renders in all consumers.

// This example is only for demonstration purposes.
// In a real-world scenario, these context values might defer.
const ClickProvider = ({ children }) => {
  const [count, setCount] = useState(0);

  // We guard the function reference with useCallback
  // to prevent unnecessary re-renders for components
  // which only depend on the increase() handler.
  const increase = useCallback(() => (
    setCount((prevCount) => prevCount + 1)
  ), []);

  // If the component rerenders for any reason,
  // and only if count is changing,
  // a new function reference is created.
  const contextValue = useMemo(() => ({
    increase,
    track: () => {
      sendCountAnalytics(count)
    },
    // other handlers
  }), [count]);

  return (
    <ClickContext.Provider value={contextValue}>
      {children}
    </ClickContext.Provider>
  );
};

Don't

Every re-render will create a new function reference and cause all consumers to re-render.

const ClickProvider = ({ children }) => {
  const [count, setCount] = useState(0);

  const increase = () => {
    setCount((prevCount) => prevCount + 1);
  };

  const contextValue = {
    increase,
    track: () => {
      sendCountAnalytics(count);
    },
  };

  return (
    <ClickContext.Provider value={contextValue}>
      {children}
    </ClickContext.Provider>
  );
};

#Contexts with many different consumers

Contexts with many consumers can lead to slow and also unnecessary rerendering, since an update to one of the context value properties will force all consumers to rerender.

In this case it will help to separate concerns and divide them into multiple smaller contexts.

A common strategy is to split context into one context of values and one context of handlers. This allows consumers to re-render only when necessary and improves rendering performance.

Do

Split the context into two separate contexts, one for the state and one for the handlers to avoid unnecessary re-renders.

interface AppState {
  count: number;
}

interface AppHandlers {
  increment: () => void;
}

const AppStateContext = createContext<AppState | undefined>(undefined);
const AppHandlersContext = createContext<AppHandlers | undefined>(undefined);

const AppProvider = ({ children }) => {
  const [count, setCount] = useState(0);
  const increment = () => setCount((prevCount) => prevCount + 1);
  const state = useMemo(() => ({ count }), [count]);
  const handlers = useMemo(() => ({ increment }), []);

  return (
    <AppStateContext.Provider value={state}>
      <AppHandlersContext.Provider value={handlers}>
        {children}
      </AppHandlersContext.Provider>
    </AppStateContext.Provider>
  );
};

const IncrementButton = () => {
  const { increment } = useContext(AppHandlersContext);
  return <button onClick={increment}>Increment</button>;
};

const Counter = () => {
  const { count } = useContext(AppStateContext);

  return (
    <div>
      <p>{count}</p>
      <IncrementButton />
    </div>
  );
};

Don't

Avoid putting all the state and handlers in one context. This will cause all consumers to re-render when any part of the context changes.

interface AppState {
  count: number;
  increment: () => void;
}

const AppContext = createContext<AppState | undefined>(undefined);

const AppProvider = ({ children }) => {
  const [count, setCount] = useState(0);
  const increment = useCallback(() => setCount((prevCount) => prevCount + 1), []);
  const state = useMemo(() => ({ count, increment }), [count]);

  return (
    <AppContext.Provider value={state}>
      {children}
    </AppContext.Provider>
  );
};

const IncrementButton = () => {
  const { increment } = useContext(AppContext);
  return <button onClick={increment}>Increment</button>;
};

const Counter = () => {
  const { count } = useContext(AppContext);
  return (
    <div>
      <p>{count}</p>
      <IncrementButton />
    </div>
  );
};

Further reading:

#Using shallow route changes

A query update with the option shallow=true will cause the entire page to be re-rendered. This is the default behavior of Next.js because it assumes that some state on the page may depend on this change. However, if the state handling is not optimized, this can result in multiple unnecessary re-renders.