Frontend Performance Optimization: Achieving Lighthouse 95+ Scores
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: