Profile the interaction before changing code
“The page is slow” is not a diagnosis. Reproduce a specific interaction on representative hardware and data: typing in a filter, opening a drawer, switching a tab, or submitting a form. Capture a React DevTools Profiler recording and a browser performance trace when main-thread work is involved. Record the baseline, the route, the input size, and the device conditions so the result can be compared later.
Commit duration
Look for commits that take long enough to interrupt an interaction.
Why it rendered
Inspect changed props, state, and context before assuming a component is at fault.
Work per interaction
Count renders, calculations, network updates, and layout changes together.
Always profile a production-like build when possible. Development-only checks can make rendering appear slower, and tiny local fixtures hide the costs of real lists, permissions, images, and network latency. Treat a profiler flamegraph as a lead, then confirm what changed with application-level measurements.
Separate an expensive render from an unnecessary render
A component may be expensive because it maps thousands of records or transforms data on every keystroke. It may also render often even though its visible output does not change. These are different problems. First inspect the component's inputs and the reason React reports for each render; then remove the root cause rather than adding memoization everywhere.
function SearchPage() {
const [query, setQuery] = useState("");
const [isHelpOpen, setHelpOpen] = useState(false);
return (
<>
<SearchResults query={query} />
<HelpDialog open={isHelpOpen} onClose={() => setHelpOpen(false)} />
</>
);
}In this example, opening help causes the parent to render. That is often fine, but if SearchResults is expensive, move the dialog state closer to the dialog or extract the results into a component whose inputs stay stable. Prefer this structural fix over making every descendant memoized. Clear component boundaries also support the data-ownership approach in our React Server Components guide.
Keep fast-changing state close to the interaction
Typing, hover state, popovers, and temporary form values should not automatically live in a high-level provider. Broad context updates can re-render every consumer, while a lifted state change can affect a whole page. Keep state at the lowest owner that needs it, split unrelated context values, and pass stable primitive values where that makes the API clearer.
- Separate a frequently changing filter value from global session or theme context.
- Use explicit loading, error, and empty states instead of repeatedly rebuilding an entire screen.
- Keep server data normalized enough that one item update does not recreate unrelated collections.
- Audit provider values that create new objects or callbacks on every render.
State design also improves resilience. A compact component can fail and recover independently when paired with the recovery patterns in our React error boundaries guide, instead of making one heavy page responsible for every state transition.
Use memoization as a measured tool, not a default
React.memo, useMemo, and useCallback trade comparison and memory costs for skipped work. They help only when the skipped work is meaningful and the inputs remain stable. A new object, array, or callback passed from a parent defeats shallow comparison; an incorrect dependency list can create stale UI bugs.
For derived data, compute it during rendering unless the calculation is proven costly. For callbacks, do not wrap every handler merely to satisfy a child's memoization. The goal is understandable code with less user-visible work, not the highest number of hooks.
Make large lists and expensive calculations proportional to what is visible
Long lists are a common source of slow commits. Paginate or virtualize when users do not need every row at once, use stable keys from data rather than indexes, and avoid sorting or filtering a large collection repeatedly in nested children. For type-ahead search, debounce the remote request and consider deferring non-urgent visual updates so the input remains responsive.
Performance work should not weaken quality checks. Add a regression test for the interaction's expected state changes, and keep broad browser coverage for flows involving focus, loading, and navigation. Our React testing strategy guide shows how unit, component, and browser tests complement each other.
Validate the result with users and production telemetry
Re-run the original profile with the same route and fixture, then compare commit duration and responsiveness. In production, track route-level web-vital data, interaction latency, JavaScript errors, and the rate of abandoned flows. Segment by device class and connection quality; an improvement on a developer laptop may not help the users who need it most.
Set a performance budget for the interaction and make it part of normal delivery. A CI budget can catch large bundle changes, while dashboards and sampled traces expose regressions caused by real data. When a regression appears, use the same measure-explain-verify loop rather than guessing from a single aggregate score.
React rendering performance checklist
✓ Reproduce one specific slow interaction
✓ Profile a production-like build with realistic data
✓ Identify why the hot component renders
✓ Improve state boundaries before adding memoization
✓ Memoize only measured expensive work
✓ Virtualize or paginate large collections
✓ Re-run the same profile after the change
✓ Monitor interaction latency in production
Build React interfaces that stay responsive as they grow
Endurance Softwares helps teams design, test, and optimize reliable React and Next.js products for real-world users.