Frontend Development 17 min read

Understanding React Performance Optimization: Update Process, Bailout, and Memoization Strategies

This article explains React's internal update loop, the render and commit phases, default bailout optimizations, and how developers can accelerate rendering using techniques such as state lifting, content lifting, React.memo, useMemo, and useCallback while avoiding over‑optimization.

ByteFE
ByteFE
ByteFE
Understanding React Performance Optimization: Update Process, Bailout, and Memoization Strategies

Performance optimization is a hot topic in front‑end development, yet many developers lack a clear understanding of when and why React components re‑render. This article builds on React's underlying update process to connect optimization techniques with their underlying principles.

Update Process

React keeps component state in sync with the UI by constructing a Virtual DOM (UI Tree) and reconciling it with the real DOM. When a user interaction triggers setState , React starts from the root node, calling getRootForUpdateFiber , scheduling the update via ScheduleUpdateOnFiber , and entering the Scheduler . The update consists of two phases: the Render Phase (building a new Work‑In‑Progress tree with effect tags) and the Commit Phase (applying the changes to the browser DOM).

The core loop repeats for each interaction, and all performance‑related work in React aims to speed up this loop.

How to Accelerate

During the Render phase, React traverses the component tree, invoking beginWork and completeWork for each node. The default optimization strategy skips nodes whose props , state , and context have not changed, entering a bailout path.

function beginWork(current, workInProgress, renderLanes) {
  // check if props or context changed
  if (oldProps !== newProps || hasLegacyContextChanged()) {
    didReceiveUpdate = true;
  } else {
    const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(
      current,
      renderLanes,
    );
    if (!hasScheduledUpdateOrContext) {
      didReceiveUpdate = false;
      // early bailout
      return attemptEarlyBailoutIfNoScheduledUpdate(current, workInProgress, renderLanes);
    }
  }
  // cannot bailout, continue normal work
}

If a node’s props, state, and context are unchanged, React returns null from attemptEarlyBailoutIfNoScheduledUpdate , meaning the subtree can be skipped.

Manual Bailout with React.memo

When the default bailout does not apply (e.g., props are new objects each render), developers can wrap a component with React.memo , which performs a shallow comparison of props. If the shallow comparison succeeds, the component is bailed out.

const Child = memo(() => {
  console.log('child render');
  return
I am child
;
});

export default function App() {
  const [count, setCount] = useState(0);
  return (
    <>
setCount(count + 1)}>update
);
}

Because the empty props object is shallow‑equal across renders, memo prevents the re‑render.

Third Path: State Lifting and Content Lifting

Instead of memoizing every component, developers can restructure the component hierarchy. By moving state down to a dedicated Counter component, the parent App no longer re‑renders unrelated children, allowing the default bailout to work.

const Child = () => {
  console.log('child render');
  return
I am child
;
};

const Counter = () => {
  const [count, setCount] = useState(0);
  return
setCount(count + 1)}>update
;
};

export default function App() {
  return (
    <>
);
}

When the state is lifted further, the child component can be passed as children to the counter, keeping its props stable and allowing bailout.

Skipping Local Construction

If a component must re‑render, developers can still reduce work using useMemo and useCallback . useMemo caches expensive calculations based on a dependency array, while useCallback memoizes functions to keep child props stable.

function updateMemoComponent(nextCreate, deps) {
  const prevDeps = prevState[1];
  if (areHookInputsEqual(nextDeps, prevDeps)) {
    return prevState[0];
  }
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

function updateCallback(callback, deps) {
  const prevDeps = prevState[1];
  if (areHookInputsEqual(deps, prevDeps)) {
    return prevState[0];
  }
  hook.memoizedState = [callback, deps];
  return callback;
}

These hooks avoid repeated heavy calculations and prevent child‑component memoization from breaking due to new function references.

Summary and Recommendations

React first attempts its built‑in bailout; developers should design components to satisfy this by state lifting or content lifting.

If bailout fails, use React.memo , PureComponent , or ShouldComponentUpdate to add another bailout layer.

When a re‑render is unavoidable, apply useMemo and useCallback to minimize the work inside the render.

Provide stable key props for list items to help React’s diff algorithm.

Avoid over‑optimizing; memoization has its own cost and can make code harder to maintain.

When performance issues arise, locate the hot component with the React Profiler and then apply the appropriate optimization technique.

References

[1] React Profiler: https://reactjs.org/blog/2018/09/10/introducing-the-react-profiler.html

[2] React Forget: https://www.youtube.com/watch?v=lGEMwh32soc

[3] Discussion on useMemo & useCallback: https://www.joshwcomeau.com/react/usememo-and-usecallback/

[4] Before you memo: https://overreacted.io/before-you-memo/

Performance OptimizationReactmemoBailoutState LiftinguseCallbackuseMemo
ByteFE
Written by

ByteFE

Cutting‑edge tech, article sharing, and practical insights from the ByteDance frontend team.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.