HomeAbout Me

React Performance: Expensive Calculations

By Daniel Nguyen
Published in React JS
July 15, 2025
2 min read
React Performance: Expensive Calculations

⚡ 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

Previous Article
React Performance: Code Splitting

Table Of Contents

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

Related Posts

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

Quick Links

About Me

Legal Stuff

Social Media