⚡ 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.
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
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.
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.
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!
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)})}
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. ⚡
Quick Links
Legal Stuff
Social Media