Home
React
React Performance: Expensive Calculations
July 15, 2025
2 min

Table Of Contents

01
🧮 The Problem: Re-Calculating Every Render
02
✅ Solution: Memoization with useMemo
03
🎯 Real-World Example: Filtering Cities
04
🔍 Step 1: Measuring Performance with DevTools
05
🧠 Step 2: Wrap in useMemo
06
🧵 Step 3: Moving to a Web Worker
07
🧑‍💻 Step 4: Creating the Worker
08
🌀 Step 5: Render Async Results with Suspense
09
🧪 Step 6: Validate the Win 🎉
10
🧠 Summary
11
📚 Resources

⚡ Speeding Up Expensive Calculations in React (with useMemo, Web Workers & Suspense)

Modern React development gives us superpowers: composable state, hooks, and declarative UI. But along with great power comes… well, performance pitfalls.

One sneaky performance bottleneck? Expensive calculations inside your render functions.

In this post, we’ll explore how to identify these issues and fix them using:

  • useMemo for memoizing heavy functions
  • Web Workers for offloading work from the main thread
  • Suspense + async rendering for smoother UX

Let’s walk through these one step at a time. 🧠💡


🧮 The Problem: Re-Calculating Every Render

Consider this example:

function Distance({ x, y }) {
const distance = calculateDistance(x, y)
return <div>The distance between {x} and {y} is {distance}.</div>
}

Even if the x and y values don’t change, if the parent re-renders or local state changes, calculateDistance runs again. For heavy calculations or large data sets, this is a performance killer.


✅ Solution: Memoization with useMemo

React provides the useMemo hook to optimize these situations:

function Distance({ x, y }) {
const distance = useMemo(() => calculateDistance(x, y), [x, y])
return <div>The distance between {x} and {y} is {distance}.</div>
}

Now, calculateDistance only runs when x or y changes. Simple and effective.

But don’t overuse useMemo—it adds complexity. Learn when it’s appropriate here: 👉 When to useMemo and useCallback


🎯 Real-World Example: Filtering Cities

Let’s say you have a dropdown with thousands of cities. To help users search efficiently, you’re using a library like match-sorter:

const results = searchCities(userInput)

This function is slow, especially on weaker devices.


🔍 Step 1: Measuring Performance with DevTools

Before optimizing, let’s measure.

  1. Run your app in production mode:

    npm run build
    npm run preview
  2. Open Chrome DevTools → Performance tab

  3. Click the gear ⚙️ and enable CPU throttling: 6x slowdown

  4. Start a performance recording, interact with the combobox, and stop the recording.

Look for the searchCities call in the flame graph. Is it getting called every time? Even when the search hasn’t changed?

It probably is. Let’s fix that.


🧠 Step 2: Wrap in useMemo

Memoize the result of searchCities:

const results = useMemo(() => searchCities(input), [input])

Now it only runs when the input changes. Rerendering the component alone won’t trigger the expensive function.

💡 Verify it: In the Performance tab, search for searchCities in the call tree. If you’ve done it right, it shouldn’t appear during unrelated rerenders.


🧵 Step 3: Moving to a Web Worker

Even when it only runs once, searchCities is still blocking the main thread. This freezes the UI on slower devices.

Rather than dumbing down our sorting algorithm, let’s move the work off the main thread with a Web Worker.

Why Web Workers?

Web Workers allow you to run JS in the background without blocking UI interactions. Perfect for CPU-intensive tasks.

We’ll use:


🧑‍💻 Step 4: Creating the Worker

Let’s refactor our city search into a worker file.

// filter-cities.worker.ts
import { expose } from 'comlink'
import { searchCities } from './search-utils'
const api = {
filterCities: (query: string) => searchCities(query),
}
expose(api)

Now in our app:

import { wrap } from 'comlink'
import Worker from './filter-cities.worker?worker'
const worker = new Worker()
const api = wrap<WorkerApi>(worker)
async function handleSearch(query: string) {
const cities = await api.filterCities(query)
setResults(cities)
}

✨ You now have a performant, non-blocking way to search cities!


🌀 Step 5: Render Async Results with Suspense

Let’s improve the experience further by using React’s use hook and Suspense to handle async rendering.

const citiesPromise = useMemo(() => api.filterCities(query), [query])
const cities = use(citiesPromise)
return (
<Suspense fallback={<div>Loading cities...</div>}>
<CityList cities={cities} />
</Suspense>
)

Add useTransition for smooth state transitions without janky UI:

const [isPending, startTransition] = useTransition()
const handleInput = (query: string) => {
startTransition(() => {
setQuery(query)
})
}

🧪 Step 6: Validate the Win 🎉

Repeat your DevTools performance trace:

  • Record input changes
  • Observe flame graph and UI responsiveness
  • Search for searchCities or blocking operations

You should see a drastic improvement! 🚀


🧠 Summary

TechniquePurpose
useMemoAvoid recalculating on every render
Web WorkersOffload expensive work from main thread
comlinkSimplify worker communication
useTransitionKeep UI responsive during state updates
Suspense + useRender async results declaratively

📚 Resources


Performance is a user experience issue—and React gives us the tools to fix it.

Use them wisely. ⚡


Tags

#ReactPerformance

Share

Related Posts

React Performance: Windowing
React Performance: Windowing
July 17, 2025
2 min
© 2025, All Rights Reserved.
Powered By

Social Media

githublinkedinyoutube