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.
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/
ByteFE
Cutting‑edge tech, article sharing, and practical insights from the ByteDance frontend team.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.