How I Scored 99/100 Performance on Lighthouse: Real Next.js Optimizations with Code
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.

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 importsI 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
widthandheightattributes (eliminates layout shift)The
priorityattribute 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:
Reduce what you send — code splitting, dynamic imports, tree shaking
Optimize the critical path — inline critical CSS, preload LCP images, minimize render-blocking resources
Reserve layout space — explicit image dimensions prevent CLS
Cache everything — service workers, HTTP caching, CDN edge caching
Keep the main thread free — async third-party scripts, avoid long tasks

