How to Optimise a Next.js Web App

Practical techniques to fix your Next.js Lighthouse score — bundle analysis, caching strategies, React Compiler, and the next.config flags nobody talks about.

12 min read May 13, 2026

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+):

Terminal
npx next experimental-analyze

You'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.

next.config.js
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'.