Back to Blog
PerformanceFrontendNext.jsOptimizationWeb Vitals

Frontend Performance Optimization: Achieving Lighthouse 95+ Scores

6 min read
By Arman Hazrati

A comprehensive guide to frontend performance optimization, covering techniques for achieving Lighthouse scores of 95+ and improving Core Web Vitals in production applications.

Frontend Performance Optimization: Achieving Lighthouse 95+ Scores

Performance is not just a nice-to-have—it's a critical factor that directly impacts user experience, conversion rates, and SEO rankings. In this article, I'll share proven techniques for optimizing frontend performance and achieving Lighthouse scores of 95+ in production applications.

Understanding Core Web Vitals

Google's Core Web Vitals measure three key aspects of user experience:

1. Largest Contentful Paint (LCP)

Target: < 2.5 seconds

LCP measures loading performance. It marks the point when the largest content element becomes visible.

2. First Input Delay (FID)

Target: < 100 milliseconds

FID measures interactivity. It's the time from when a user first interacts with your page to when the browser responds.

3. Cumulative Layout Shift (CLS)

Target: < 0.1

CLS measures visual stability. It quantifies how much visible content shifts during page load.

Optimization Strategies

1. Code Splitting and Lazy Loading

Split your JavaScript bundles and load code only when needed.

// Dynamic imports for code splitting
import dynamic from 'next/dynamic'

// Lazy load heavy components
const HeavyComponent = dynamic(() => import('./HeavyComponent'), {
  loading: () => <LoadingSpinner />,
  ssr: false, // Disable SSR if not needed
})

// Route-based code splitting (automatic in Next.js)
// Each route gets its own bundle

Benefits:

  • Smaller initial bundle size
  • Faster Time to Interactive (TTI)
  • Better caching strategies

2. Image Optimization

Images are often the largest assets. Optimize them aggressively.

// Next.js Image component with optimization
import Image from 'next/image'

<Image
  src="/hero.jpg"
  alt="Hero image"
  width={1200}
  height={600}
  priority // Load immediately for above-the-fold images
  placeholder="blur" // Show blur placeholder
  blurDataURL={blurDataUrl}
/>

// Responsive images
<Image
  src="/product.jpg"
  alt="Product"
  width={800}
  height={600}
  sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
  srcSet="/product-400.jpg 400w, /product-800.jpg 800w, /product-1200.jpg 1200w"
/>

Optimization Techniques:

  • Use WebP format with fallbacks
  • Implement responsive images
  • Lazy load below-the-fold images
  • Use blur placeholders

3. Font Optimization

Fonts can block rendering. Optimize font loading.

// Next.js font optimization
import { Inter } from 'next/font/google'

const inter = Inter({
  subsets: ['latin'],
  display: 'swap', // Show fallback font immediately
  preload: true,
  variable: '--font-inter',
})

// In your CSS
html {
  font-family: var(--font-inter), sans-serif;
}

Best Practices:

  • Use font-display: swap
  • Preload critical fonts
  • Subset fonts to only needed characters
  • Use system fonts when possible

4. CSS Optimization

Optimize CSS delivery and remove unused styles.

// Critical CSS inlining
// Extract critical CSS for above-the-fold content
const criticalCSS = extractCriticalCSS(html)

// Inline critical CSS
<head>
  <style dangerouslySetInnerHTML={{ __html: criticalCSS }} />
  <link rel="stylesheet" href="/styles.css" media="print" onLoad="this.media='all'" />
</head>

// Remove unused CSS with PurgeCSS
// tailwind.config.js
module.exports = {
  content: [
    './pages/**/*.{js,ts,jsx,tsx}',
    './components/**/*.{js,ts,jsx,tsx}',
  ],
  // PurgeCSS automatically removes unused styles
}

5. JavaScript Optimization

Minimize and optimize JavaScript execution.

// Tree shaking (automatic with modern bundlers)
// Only import what you need
import { debounce } from 'lodash-es' // ✅ Good
import _ from 'lodash' // ❌ Bad (imports entire library)

// Use Web Workers for heavy computations
// worker.ts
self.onmessage = (e) => {
  const result = heavyComputation(e.data)
  self.postMessage(result)
}

// main.ts
const worker = new Worker(new URL('./worker.ts', import.meta.url))
worker.postMessage(data)
worker.onmessage = (e) => {
  console.log('Result:', e.data)
}

6. Caching Strategies

Implement effective caching for static and dynamic content.

// Service Worker for offline caching
// sw.js
const CACHE_NAME = 'app-v1'
const urlsToCache = [
  '/',
  '/styles.css',
  '/app.js',
]

self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => {
      return cache.addAll(urlsToCache)
    })
  )
})

self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then((response) => {
      return response || fetch(event.request)
    })
  )
})

// HTTP caching headers
// next.config.js
module.exports = {
  async headers() {
    return [
      {
        source: '/static/:path*',
        headers: [
          {
            key: 'Cache-Control',
            value: 'public, max-age=31536000, immutable',
          },
        ],
      },
    ]
  },
}

7. Prefetching and Preloading

Preload critical resources and prefetch likely next pages.

// Preload critical resources
<link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossOrigin="anonymous" />
<link rel="preload" href="/hero-image.jpg" as="image" />

// Prefetch likely next pages
<Link href="/about" prefetch>
  About
</Link>

// DNS prefetch for external resources
<link rel="dns-prefetch" href="https://api.example.com" />

8. Reduce JavaScript Execution Time

Minimize main thread blocking.

// Debounce expensive operations
import { debounce } from 'lodash-es'

const handleScroll = debounce(() => {
  // Expensive scroll handler
}, 100)

// Use requestIdleCallback for non-critical work
requestIdleCallback(() => {
  // Analytics, logging, etc.
})

// Virtualize long lists
import { FixedSizeList } from 'react-window'

<FixedSizeList
  height={600}
  itemCount={10000}
  itemSize={50}
  width="100%"
>
  {Row}
</FixedSizeList>

Next.js Specific Optimizations

1. Static Site Generation (SSG)

Pre-render pages at build time.

// getStaticProps for static generation
export async function getStaticProps() {
  const data = await fetchData()
  
  return {
    props: { data },
    revalidate: 60, // ISR: regenerate every 60 seconds
  }
}

2. Incremental Static Regeneration (ISR)

Update static pages incrementally.

// ISR with revalidate
export async function getStaticProps({ params }) {
  const product = await getProduct(params.slug)
  
  return {
    props: { product },
    revalidate: 3600, // Regenerate every hour
  }
}

3. Server Components

Reduce client-side JavaScript with React Server Components.

// Server Component (no JavaScript sent to client)
async function ProductList() {
  const products = await getProducts()
  
  return (
    <ul>
      {products.map((product) => (
        <li key={product.id}>{product.name}</li>
      ))}
    </ul>
  )
}

Monitoring and Measurement

Lighthouse CI

Automate Lighthouse testing in CI/CD.

# .github/workflows/lighthouse.yml
name: Lighthouse CI

on: [push, pull_request]

jobs:
  lighthouse:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v2
      - run: npm install
      - run: npm run build
      - uses: treosh/lighthouse-ci-action@v7
        with:
          urls: |
            http://localhost:3000
          uploadArtifacts: true
          temporaryPublicStorage: true

Real User Monitoring (RUM)

Track Core Web Vitals in production.

// Web Vitals measurement
import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals'

function sendToAnalytics(metric) {
  // Send to your analytics service
  analytics.track('web-vital', {
    name: metric.name,
    value: metric.value,
    id: metric.id,
  })
}

getCLS(sendToAnalytics)
getFID(sendToAnalytics)
getFCP(sendToAnalytics)
getLCP(sendToAnalytics)
getTTFB(sendToAnalytics)

Performance Checklist

  • Code splitting implemented
  • Images optimized (WebP, lazy loading, responsive)
  • Fonts optimized (preload, subset, swap)
  • CSS optimized (critical CSS, purge unused)
  • JavaScript minified and tree-shaken
  • Caching strategy implemented
  • Prefetching/preloading critical resources
  • Service Worker for offline support
  • Core Web Vitals monitored
  • Lighthouse CI in place

Conclusion

Achieving Lighthouse scores of 95+ requires a comprehensive approach to performance optimization. Focus on the Core Web Vitals, implement the strategies outlined above, and continuously monitor and measure your performance in production.

Remember: performance is not a one-time optimization—it's an ongoing process that requires continuous attention and improvement.


Tools & Resources: