"It works, but it feels sluggish." That's one of the most common things clients say about a React or Next.js app — and almost always, the slowness comes down to a handful of fixable causes. Here are the seven I run into most, with the concrete fix for each.
1. Unnecessary re-renders
The single most common culprit. A parent re-renders, and every child re-renders with it — even the ones whose props never changed.
How to spot it: open React DevTools, enable "Highlight updates when components render," and click around. If half the screen flashes when you type into one input, you have a re-render problem.
Fix: wrap pure children in React.memo, stabilise callbacks with useCallback, memoise derived values with useMemo. Don't sprinkle these everywhere — measure first, then memoise the components that actually re-render hot.
2. State that lives too high
When you keep form state at the top of a large tree, every keystroke re-renders everything below it.
Fix: push state down to the smallest component that needs it. For genuinely global state (auth, theme, cart), use a focused store like Zustand and subscribe to one slice, so only the components using that slice re-render.
3. Oversized JavaScript bundles
If your app ships 1.5 MB of JS before anything is interactive, no micro-optimisation saves the first impression.
Fix: code-split by route and lazy-load heavy components with next/dynamic or React.lazy. Audit the bundle and look for accidental imports — pulling one helper from a giant library often drags the whole library in.
4. Images shipped at full size
A hero "image" that's a 4000px, 3 MB PNG tanks your Largest Contentful Paint on mobile every time.
Fix: use next/image so images are resized, served in modern formats (WebP/AVIF), and lazy-loaded below the fold. Set explicit width/height to avoid layout shift.
5. Blocking data fetching
If a page waits for one slow API call before rendering anything, users stare at a blank screen.
Fix: render the shell immediately and stream or progressively load data. Fetch on the server where it makes sense, cache aggressively, and use Suspense or skeletons so the page feels alive.
6. Effects that fire too often
A useEffect with the wrong dependency array can run on every render — re-fetching, re-subscribing, or looping.
Fix: get the dependency array right, debounce expensive effects (like search-as-you-type), and clean up subscriptions in the return function.
7. Doing too much on the main thread
Heavy synchronous work — parsing big JSON, sorting thousands of rows — blocks the thread that also handles clicks and scrolls. The result is jank.
Fix: move heavy work to a Web Worker, virtualise long lists so you only render what's on screen, and memoise expensive computations. Often the real win is not doing the work at all — paginate or filter on the server.
Approach it in order
- Measure first — Lighthouse for the page-level picture, React DevTools Profiler for re-renders.
- Fix the biggest lever first — usually bundle size or images for first load, re-renders for interaction speed.
- Re-measure — confirm the number moved before moving on.
Most "slow app" complaints are solved by two or three of these — often a single focused afternoon.
Stuck on a React or Next.js app that feels slow and not sure which cause it is? I profile it, fix the highest-impact issues, and hand you the before/after numbers — vengstudio.online.
Top comments (0)