How I Scored 99/100 Performance on Lighthouse: Real Next.js Optimizations with Code

Performancenextjsperformancejavascriptreactweb vitalsspeed
by flavglen

Achieving a near-perfect Lighthouse score requires deliberate optimization. This post breaks down the exact strategies I used on my portfolio site — including aggressive code splitting with next/dynamic, inline critical CSS, and next-gen image formats — with real code snippets you can apply to your own Next.js projects.

Did you find this helpful?
-
How I Scored 99/100 Performance on Lighthouse: Real Next.js Optimizations with Code

I recently ran Lighthouse on my portfolio site (flavglen.dev) and hit a perfect 99/100 Performance score with 95/100 Accessibility, 96/100 Best Practices, and 90/100 SEO. None of this happened by accident. This post walks through every major optimization I implemented — with real code snippets from my Next.js repository — so you can apply the same strategies to your own projects.

1. Aggressive Code Splitting with next/dynamic and Suspense

The single highest-impact decision was splitting everything below the fold into separate bundles using Next.js's dynamic() import with Suspense boundaries. This ensures the critical rendering path only ships code for what's visible on the initial viewport.

Instead of importing components statically:

// ❌ Before: All components loaded eagerly
import { AboutSection } from "@/components/sections/AboutSection"
import { ProjectsSection } from "@/components/sections/ProjectsSection"
// ...more imports

I deferred everything below the hero section:

// ✅ After: Dynamic imports with Suspense
import { Suspense } from "react"
import dynamic from "next/dynamic"
import { Header } from "@/components/sections/Header"
import { HeroSection } from "@/components/sections/HeroSection"
import { StatsSection } from "@/components/sections/StatsSection"

// Dynamically import below-the-fold sections to reduce initial bundle size
const AboutSection = dynamic(() => import("@/components/sections/AboutSection"), {
  loading: () => <div className="py-6 sm:py-8 md:py-12" />,
})

const ProjectsSection = dynamic(() => import("@/components/sections/ProjectsSection"), {
  loading: () => <div className="py-6 sm:py-8 md:py-12" />,
})

Then wrapped each in a <Suspense> boundary:

<Suspense fallback={<div className="py-6 sm:py-8 md:py-12" />}>
  <ProjectsSection featured />
</Suspense>

Impact: The initial JavaScript payload dropped significantly. The browser only downloads, parses, and executes code required for the hero section before painting — everything else streams in later.

Key takeaway: if it's not in the initial viewport, it should be a dynamic import.

2. Inline Critical CSS

Next.js 16 introduced experimental.inlineCss, which automatically inlines critical CSS directly into the HTML response. This eliminates render-blocking stylesheet requests entirely.

// next.config.mjs
const nextConfig = {
  experimental: {
    inlineCss: true, // Inlines critical CSS directly into the HTML
  },
}

Warning:  Since it's under the experimental flag, it's not guaranteed to be stable


Impact: Zero render-blocking CSS requests. The browser can start painting immediately without waiting for external stylesheets. Lighthouse confirmed: "Render-blocking requests" shows no items.

3. Next-Generation Image Formats with Explicit Dimensions

Every image on the site uses Next.js's <Image> component with:

  • WebP format (automatically negotiated via Next.js image optimization)

  • Explicit width and height attributes (eliminates layout shift)

  • The priority attribute on above-the-fold images (bypasses lazy-loading for LCP elements)

import Image from "next/image"

// Above-the-fold hero image — priority loaded, explicit dimensions
<Image
  src="/dp.jpeg"
  alt="Glen Pais — AI-Enhanced Full Stack Developer"
  width={320}
  height={390}
  className="object-cover w-full h-full scale-105 hover:scale-110 transition-transform duration-700"
  priority  // ✅ Signals importance to the browser
/>

Impact: The LCP element (the profile photo) loads in 86ms. CLS is zero because every image has reserved space via explicit dimensions. The WebP conversion saves additional bandwidth over JPEG/PNG.

4. Extremely Fast Server Response (26ms TTFB)

The server responded in 26ms — well under the 200ms threshold. This comes from:

  • Next.js App Router — streaming SSR with React Server Components

  • Edge/Vercel deployment — globally distributed with minimal latency

  • Minimal server-side work — static generation where possible, streaming for dynamic content

TTFB is the start of everything. If your server is slow, none of the other optimizations can fully compensate.

5. Third-party scripts Loaded After Interaction (Non-Blocking)

Third-party scripts are notorious performance killers. Google Tag Manager loads after the page is interactive using strategy="afterInteractive":

'use client'
import Script from 'next/script'

export default function GoogleAnalytics() {
  return (
    <>
      <Script
        strategy="afterInteractive"  // ✅ Non-blocking
        src={`https://www.googletagmanager.com/gtag/js?id=${GA_TRACKING_ID}`}
      />
      <Script
        id="google-analytics"
        strategy="afterInteractive"
        dangerouslySetInnerHTML={{
          __html: `window.dataLayer = window.dataLayer || []; ...`,
        }}
      />
    </>
  )
}

Impact: The GA script loads asynchronously and doesn't block the main thread during critical rendering. The site still manages a 30ms TBT despite GA's 167KB payload

6. Zero Layout Shift (CLS = 0)

Achieving CLS of zero required a systematic approach:

a) Explicit dimensions on all images

Every includes width and height props so the browser reserves space before the image loads.

b) No dynamically injected content above the fold

All content that affects layout dimensions is present in the initial server-rendered HTML. No late-arriving elements push existing content around.

c) Font rendering doesn't cause reflow

With no custom web fonts (system font stack via Tailwind), there's no FOIT/FOUT that could cause layout shifts.

d) No CSS animations affecting layout

Animations use transform and opacity only — properties that don't trigger layout recalculation.

7. Service Worker with Strategic Caching

Using next-pwa, I set up a comprehensive caching strategy with StaleWhileRevalidate for most assets and NetworkFirst for pages:

const pwaConfig = withPWA({
  dest: 'public',
  register: true,
  skipWaiting: true,
  runtimeCaching: [
    {
      // Images: cache-first with 24h expiry
      urlPattern: /\.(?:jpg|jpeg|gif|png|svg|ico|webp)$/i,
      handler: 'StaleWhileRevalidate',
      options: {
        cacheName: 'static-image-assets',
        expiration: { maxEntries: 64, maxAgeSeconds: 24 * 60 * 60 },
      },
    },
    {
      // API calls: network-first with 10s timeout
      urlPattern: /\/api\/.*$/i,
      handler: 'NetworkFirst',
      method: 'GET',
      options: {
        cacheName: 'apis',
        expiration: { maxEntries: 16, maxAgeSeconds: 24 * 60 * 60 },
        networkTimeoutSeconds: 10,
      },
    },
    {
      // Pages: network-first for fresh content
      urlPattern: ({ request }) => request.destination === 'document',
      handler: 'NetworkFirst',
      options: {
        cacheName: 'pages',
        expiration: { maxEntries: 32, maxAgeSeconds: 24 * 60 * 60 },
      },
    },
  ],
})

This means repeat visits load from cache instantly, and the service worker updates in the background.


9. Minimal Main-Thread Work (TBT = 30ms)

Total Blocking Time of 30ms is remarkably low. The secret: ship less JavaScript. Code splitting ensures each chunk is as small as possible, and Next.js's Turbopack produces highly optimized bundles.

The Bigger Picture

A perfect Lighthouse score isn't about chasing numbers. It's about applying fundamental web performance principles:

  1. Reduce what you send — code splitting, dynamic imports, tree shaking

  2. Optimize the critical path — inline critical CSS, preload LCP images, minimize render-blocking resources

  3. Reserve layout space — explicit image dimensions prevent CLS

  4. Cache everything — service workers, HTTP caching, CDN edge caching

  5. Keep the main thread free — async third-party scripts, avoid long tasks