All posts
Nov 5, 2025·7 min read

Getting to 95+ Lighthouse on a Next.js App

Next.jsPerformanceWeb

Umesh Bhati

Full-Stack & AI Engineer

The Challenge

When building modern web applications, Next.js gives you a lot of performance features out of the box. But getting a perfect Lighthouse score, especially on mobile, requires careful attention to detail.

I recently optimized a content-heavy Next.js application that was struggling with a 65 mobile performance score. Here is exactly what moved the needle to 98.

Fonts and Images

These are usually the biggest culprits for poor LCP (Largest Contentful Paint) and CLS (Cumulative Layout Shift).

Fonts:

  • Always use `next/font`. It preloads your fonts and removes layout shifts.
  • Subset your fonts if possible. If you don't need Cyrillic characters, don't ship them.
  • Images:

  • Use `next/image` for everything.
  • Critical: add `priority={true}` to any image that appears above the fold (like hero images).
  • Always explicitly set `width` and `height` to prevent layout shifts.
  • Code Splitting and Dynamic Imports

    Next.js automatically code-splits by route, but you need to manually split heavy components that aren't immediately visible.

    import dynamic from "next/dynamic";
    
    const HeavyChart = dynamic(() => import("./HeavyChart"), {
      loading: () => <p>Loading chart...</p>,
      ssr: false // If it relies on browser APIs
    });

    If you have a complex modal or a chart that sits below the fold, dynamically import it. Your initial bundle size will thank you.

    Caching Strategy

    The App Router's caching model is powerful but confusing. To get the best server response times (TTFB):

  • **Use static rendering wherever possible.** If a page doesn't depend on user-specific data, `force-static` is your friend.
  • **Implement stale-while-revalidate (ISR)** for pages that change occasionally but need to be fast. The `revalidate` option in `fetch` is perfect for this.
  • **Database caching**: React Cache (`cache`) prevents duplicate queries in the same render pass, but you still need Redis or similar for cross-request caching.
  • Third-Party Scripts

    Analytics, customer support widgets, and ads will destroy your score.

    Use next/script with the correct strategy:

  • `beforeInteractive`: Only for critical scripts like bot detection or consent managers.
  • `afterInteractive`: The default. Good for analytics.
  • `lazyOnload`: For chat widgets and heavy third-party tools that the user doesn't immediately need.
  • The Results

    After applying these changes, our mobile performance score went from 65 to 98. LCP dropped from 4.2s to 1.1s, and CLS was effectively zero.

    The biggest lesson? Don't fight the framework. Use Next.js's built-in components (Image, Font, Script) as intended, and strictly manage your third-party dependencies.

    Questions or thoughts? Find me on X or send an email.