Your Next.js app scores a 54 on Lighthouse. You shipped it three months ago with a perfect 100, and now there's an analytics SDK, a cookie banner, two icon libraries you imported wrong, and a client component wrapping the entire layout because someone needed useState in the header. I've been there — more than once — and every time the fix follows the same rhythm: measure first, cut ruthlessly, cache the rest, and only reach for exotic tricks after the boring wins are locked in.
This is the checklist I actually run through when a Next.js app gets slow. No 'just use React Server Components' hand-waving. Just the concrete steps, in the order that gives the fastest score bump per hour of work.
1. Bundle size — the one that hurts the most
1.1 Analyse first, cut second
You cannot optimise what you cannot see. Before touching a single import, generate a bundle report. Run the built-in analyzer (Next.js 16.1+):
npx next experimental-analyzeYou'll get a treemap showing exactly which packages eat the most space. Look for the usual suspects: moment.js (328KB — replace with date-fns or the native Intl API), full lodash imports, and icon libraries where you imported the entire set instead of individual icons.
1.2 The barrel-export trap
Some packages re-export everything from a single entry point — icon libraries, utility packs, UI kits. Import one thing and you ship the whole set. Next.js has an escape hatch: optimizePackageImports. Drop the offenders into next.config.js and the compiler rewrites your imports into deep paths automatically.
module.exports = {
experimental: {
optimizePackageImports: [
"lucide-react",
"date-fns",
"@radix-ui/react-icons",
],
},
};2. Caching — the free performance win
Every request that hits your origin is a request that could have been served from the edge in 30ms. The App Router gives you four cache layers: the fetch cache, the Full Route Cache, the Router Cache, and the Data Cache. You do not need to master all four — you need to make sure you are not silently opting out of them.
- Never write `dynamic = 'force-dynamic'` unless you know why. It disables static rendering for the whole route.
- Prefer `revalidate = 60` over `no-store`. Sixty seconds of staleness is invisible to users and 10× cheaper for you.
- Tag your fetches (`{ next: { tags: ['posts'] } }`) so you can invalidate surgically with `revalidateTag` instead of nuking everything.
3. React Compiler — turn it on
The React Compiler ships stable in React 19.2 and integrates with Next.js 16 via a single flag. It auto-memoises components and hooks, killing 80% of the manual useMemo / useCallback / React.memo noise in your codebase. Enable it, delete the manual memoisation, run your tests.
Optimisation is a habit, not a project. Run the analyzer once a quarter, keep the boring wins locked in, and you'll never have to write a follow-up post titled 'How I fixed my Next.js app again'.