How to speed up a React app without rewriting everything
Most React apps aren't slow because React is slow — they're slow because components re-render when they shouldn't, or load too much code at once. The good news: you can usually speed things up without a big rewrite.
Measure first, optimize second
The first mistake is optimizing without data. Before changing anything, open the React DevTools Profiler and see which components render most often and how long each render takes.
Record a flame graph for a typical scenario: clicking a button, typing in a field, scrolling a list. It often turns out that 80% of the time goes into one or two components you never suspected.
React.memo and when it doesn't help
React.memo prevents a component from re-rendering if its props haven't changed. Sounds simple, but there's a catch: if the parent creates new objects or functions in JSX on every render, memo won't help, because the prop comparison by reference returns false.
For memo to work, props need to be stable. Wrap functions in useCallback and objects in useMemo. Note that useMemo itself isn't free either — only add it where the computation is genuinely expensive.
- memo is effective for leaf components with primitive props
- useCallback is needed when a function is passed into a memo component
- useMemo is justified for filtering/sorting large arrays
- Don't wrap everything — there's always overhead
Code splitting: load only what's needed right now
If all your JavaScript loads on the first page visit, that's almost always too much. React.lazy and Suspense let you load components on demand: modals, tabs, heavy forms — anything the user doesn't see right away.
In Next.js it's even easier: a dynamic import with { ssr: false } automatically moves a component into a separate chunk. The result — less code on first load, faster Time to Interactive.
Virtualization: when the list is long
If you're rendering more than 100-200 items at once, virtualization is probably worth it. Libraries like @tanstack/virtual or react-window only render the visible rows, not the whole list.
The gain can be huge: a list of 1000 rows without virtualization renders in seconds; with virtualization, in milliseconds. The difference is especially noticeable on mobile.
What's not worth touching
Not every optimization is worth the time. If a component renders in 2ms, that's fine. If a list has 20 items, virtualization isn't needed. Premature optimization adds complexity without real benefit.
A good rule: if users aren't complaining about lag and the profiler doesn't show any hot spots, things are already working well enough.