⚡ 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 functionsLet’s walk through these one step at a time. 🧠💡
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.
useMemoReact 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
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.
Before optimizing, let’s measure.
Run your app in production mode:
npm run buildnpm run preview
Open Chrome DevTools → Performance tab
Click the gear ⚙️ and enable CPU throttling: 6x slowdown
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.
useMemoMemoize 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.
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.
Web Workers allow you to run JS in the background without blocking UI interactions. Perfect for CPU-intensive tasks.
We’ll use:
comlink to simplify messaging between the main thread and workerLet’s refactor our city search into a worker file.
// filter-cities.worker.tsimport { 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!
SuspenseLet’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)})}
Repeat your DevTools performance trace:
searchCities or blocking operationsYou should see a drastic improvement! 🚀
| Technique | Purpose |
|---|---|
useMemo | Avoid recalculating on every render |
| Web Workers | Offload expensive work from main thread |
comlink | Simplify worker communication |
useTransition | Keep UI responsive during state updates |
Suspense + use | Render async results declaratively |
Performance is a user experience issue—and React gives us the tools to fix it.
Use them wisely. ⚡