Skip to main content

React performance starts with measurement

August 19, 20215 min read

Most React performance discussions start with a solution.

Someone sees a slow screen and adds useMemo, useCallback, or React.memo. Sometimes that helps. Sometimes it only makes the code harder to read.

While I led the Quote & Bind team at iptiQ, I gave an internal workshop about React performance. We worked on configurable insurance journeys with many screens, rules, and reusable components.

I wanted the team to share one simple habit: measure first, then optimize the work that matters.

Start with the slow interaction

“The application feels slow” is not enough information.

Which interaction feels slow? Does the delay happen during loading, typing, rendering, or a network request? Does it happen on every device?

The workshop started with the browser tools and React Profiler. We looked for three things:

  • Code the browser loaded but did not need yet.
  • Expensive calculations that ran again with the same input.
  • Components that rendered even when their visible output stayed the same.

This gave us a specific problem before we changed any code.

Load less code

The first example used a search screen with a large list of Swiss postcodes. The initial page did not need the search code or its data.

React.lazy moved that work into a separate bundle.

src/App.js
const Search = React.lazy(() => import('./Search'))
 
<React.Suspense fallback={<p>Loading search...</p>}>
  {showSearch ? <Search /> : null}
</React.Suspense>

The Network panel showed when the browser requested the bundle. The Coverage panel showed how much JavaScript the first screen did not use.

Lazy loading did not make the search code faster. It stopped that code from delaying users who had not opened search.

Calculate less

The next example filtered the postcode list. An intentionally expensive function made the problem easy to see.

The component ran that function after every render. useMemo reused the result until the filter changed.

src/Search.js
const zipcodes = useMemo(() => getItems(filter), [filter]);

This is a good use of memoization because the calculation is expensive and has a clear input.

It does not mean every calculation needs useMemo. A simple expression is usually cheaper than the memoization around it.

We also moved the calculation into a Web Worker. That version did not reduce the work. It kept the main thread free, so typing stayed responsive during the calculation.

Render less

React can render a component without changing the DOM. The render still runs the component code and creates its next element tree.

React.memo can skip that work when the component receives the same props.

src/Search.js
function Entry({ zipcode, place }) {
  return <li>{`${zipcode}: ${place}`}</li>;
}
 
const MemoEntry = React.memo(Entry);

The important part is “the same props.” A new object, array, or function has a new reference. That reference can make the component render again.

Before adding more memoization, I first check whether state sits too high in the tree. Moving state closer to its user often removes the unwanted renders with less code.

Keep context updates focused

Context makes shared state convenient. It can also make an update reach more components than expected.

Imagine one context that contains the theme, language, and user settings. Changing the language updates every component that reads that context, including a component that only needs the theme.

Splitting values by purpose gives each update a smaller boundary.

src/Preferences.js
const value = useMemo(() => ({ theme, setTheme }), [theme]);
 
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;

The workshop example uses separate theme and language contexts. A language change can update the greeting without rendering the theme preview.

React.memo cannot block an update from a context that the component reads. The context boundary must make sense first.

Measure what users receive

The Profiler explains what happened on one device during one test. Production users bring different devices, networks, data, and navigation paths.

The final topic was production monitoring. I later connected reportWebVitals to a small reporter.

src/index.js
reportWebVitals(process.env.NODE_ENV === 'production' ? sendToAnalytics : logger);

Development builds log each metric. Production builds send a small JSON payload to a configured endpoint.

The example tries navigator.sendBeacon first. It uses a request with keepalive when the browser cannot queue the beacon.

The important work starts after collection. Group results by page and metric. Review percentiles, not only averages. Averages can hide the visits that need attention.

The rule I wanted the team to keep

Performance tools are useful when they solve a measured problem.

React.lazy can reduce the first load. useMemo can avoid repeated calculations. React.memo can skip renders. Smaller context boundaries can contain updates.

None of them answers the first question: what is slow for the user?

My process is still the same:

  1. Find the slow interaction.
  2. Measure it with the browser tools and React Profiler.
  3. Identify the work that the user can avoid.
  4. Make the smallest useful change.
  5. Measure again.
  6. Check production results.

That process matters more than any single React API.

Workshop material

I kept the workshop examples in a React performance repository. Its branches show the starting examples and their solutions.

The repository still uses React 17 and Create React App 4. It supports the original workshop. It is not a starter for a new application.

I restored two unfinished examples in 2026. I kept their commit dates near the workshop period, when I started managing the Quote & Bind team. The update date on this post records that later work.