React applications can be fast. Many are not. The performance problems we encounter most often in production React apps are almost always in the same categories. Here are the 8 most common causes of a slow React app and how to fix them.
Bundling your entire React application into a single JavaScript file means users download every component, even ones they will never visit. For a medium-sized app, this can mean 2-5MB of JavaScript loaded before anything renders. React.lazy() and dynamic import() let you load components only when they are needed. In Next.js, route-based splitting happens automatically.
Fix: Use React.lazy() for any component that is not needed on initial page load. Use Suspense to show a fallback while the component loads.
React re-renders a component whenever its state or props change. When parent components re-render, all child components re-render too, even if their props have not changed. In complex component trees, this cascades into performance problems. Use React DevTools Profiler to identify which components are re-rendering most frequently.
Fix: Wrap expensive child components in React.memo(). Use useMemo() for expensive calculations. Use useCallback() to stabilize function references passed as props.
Rendering 1,000 table rows or list items means 1,000 DOM nodes. The browser has to paint, layout, and manage all of them simultaneously. This is one of the easiest performance killers to avoid.
Fix: Use react-window or react-virtual to render only the visible rows. Lists of 10,000 items render as smoothly as lists of 20 with windowing enabled.
Images are typically the largest assets on any web page. Using PNG where WebP works, serving full-resolution images to mobile viewports, and not lazy loading below-the-fold images are all common and expensive mistakes.
Fix: Use Next.js Image component or a similar tool that handles WebP conversion, responsive sizing, and lazy loading automatically.
Filtering a large array, sorting data, or running complex transformations on every render adds CPU time on every state update, even when the underlying data has not changed.
Fix: Wrap expensive calculations in useMemo() with the correct dependency array. The calculation only re-runs when its inputs change.
Components that fetch data in useEffect on every render, or parent and child components that both fetch the same data independently, create network waterfalls and redundant requests.
Fix: Use React Query or SWR for data fetching. Both handle caching, deduplication, and background revalidation automatically, eliminating most redundant network requests.
Event listeners, timers, and subscriptions that are set up in useEffect but never cleaned up continue running after the component unmounts. In long-running apps, these accumulate and degrade performance progressively.
Fix: Always return a cleanup function from useEffect when setting up subscriptions, timers, or event listeners.
Moment.js (67KB), Lodash (70KB+), and many UI libraries add significant weight when imported entirely. Many projects import entire libraries when they only use one or two functions.
Fix: Use bundle analysis (webpack-bundle-analyzer or next/bundle-analyzer) to see what is largest in your bundle. Replace moment.js with date-fns. Import specific Lodash functions rather than the whole library.
We diagnose and fix frontend performance issues and can show you measurable improvements.
Talk to Vola