Achieving Perfect Lighthouse Scores
When we first ran Lighthouse on our client's e-commerce platform, the results were... humbling. A performance score of 62, with red metrics across the board. Three months later, we achieved a consistent 100. Here's how we did it.
The Starting Point
Our initial Lighthouse audit revealed several critical issues:
- LCP (Largest Contentful Paint): 4.2s ❌ (target: <2.5s)
- FID (First Input Delay): 180ms ❌ (target: <100ms)
- CLS (Cumulative Layout Shift): 0.25 ❌ (target: <0.1)
- Performance Score: 62/100 ❌
Info
Core Web Vitals are Google's metrics for measuring user experience. They directly impact SEO rankings and, more importantly, user satisfaction and conversion rates.
Strategy 1: Image Optimization
Images accounted for 78% of our page weight. We implemented:
Next.js Image Component
Switching to Next.js's <Image> component gave us automatic optimization:
// Before: Standard img tag
<img src="/hero.jpg" alt="Hero" />
// After: Next.js Image with lazy loading
import Image from 'next/image';
<Image
src="/hero.jpg"
alt="Hero"
width={1200}
height={600}
priority // for above-the-fold images
placeholder="blur"
blurDataURL="data:image/jpeg;base64,..."
/>
Impact: LCP improved from 4.2s → 3.1s
WebP with AVIF Fallback
We converted all images to modern formats:
<picture>
<source srcSet="/hero.avif" type="image/avif" />
<source srcSet="/hero.webp" type="image/webp" />
<img src="/hero.jpg" alt="Fallback" />
</picture>
Impact: Page weight reduced by 64%
Strategy 2: Code Splitting
Our bundle size was massive: 847KB gzipped. We implemented aggressive code splitting:
// Dynamic imports for heavy components
import dynamic from 'next/dynamic';
const HeavyChart = dynamic(() => import('@/components/Chart'), {
loading: () => <Skeleton />,
ssr: false // Don't render on server
});
const ProductGallery = dynamic(
() => import('@/components/ProductGallery'),
{ ssr: true }
);
We also analyzed our bundle with @next/bundle-analyzer:
npm run build -- --analyze
Tip
We discovered that moment.js (which we rarely used) was adding 288KB! We replaced it with date-fns and only imported the functions we needed, saving 270KB.
Impact: Initial bundle size reduced from 847KB → 312KB
Strategy 3: Font Optimization
Custom fonts were causing significant layout shift. We fixed this with font-display strategies:
@font-face {
font-family: 'CustomFont';
src: url('/fonts/custom.woff2') format('woff2');
font-display: swap; /* Use fallback font immediately */
font-weight: 400;
}
/* Define fallback font with similar metrics */
.text {
font-family: 'CustomFont', -apple-system, system-ui, sans-serif;
}
We also preloaded critical fonts:
// In _document.tsx or layout.tsx
<link
rel="preload"
href="/fonts/custom.woff2"
as="font"
type="font/woff2"
crossOrigin="anonymous"
/>
Impact: CLS improved from 0.25 → 0.08
Strategy 4: JavaScript Optimization
Virtualization for Long Lists
Product listings were rendering 500+ items at once. We implemented virtualization:
import { FixedSizeList } from 'react-window';
function ProductList({ items }) {
return (
<FixedSizeList
height={600}
itemCount={items.length}
itemSize={120}
width="100%"
>
{({ index, style }) => (
<ProductCard
style={style}
product={items[index]}
/>
)}
</FixedSizeList>
);
}
Impact: FID improved from 180ms → 45ms
React.memo for Expensive Components
We wrapped expensive components to prevent unnecessary re-renders:
const ProductCard = React.memo(({ product }) => {
return (
<div className="product-card">
{/* ... */}
</div>
);
}, (prevProps, nextProps) => {
// Custom comparison
return prevProps.product.id === nextProps.product.id;
});
Warning
Don't over-optimize! React.memo has overhead. Only use it for components that re-render frequently with the same props.
Strategy 5: Critical CSS
We inlined critical CSS to prevent render-blocking:
// Extract critical CSS for above-the-fold content
import { getCriticalCSS } from '@/lib/critical-css';
export default function RootLayout({ children }) {
const criticalCSS = getCriticalCSS();
return (
<html>
<head>
<style dangerouslySetInnerHTML={{ __html: criticalCSS }} />
</head>
<body>{children}</body>
</html>
);
}
The Results
After systematic optimization:
| Metric | Before | After | Change | |--------|--------|-------|--------| | LCP | 4.2s | 1.8s | ✅ -57% | | FID | 180ms | 45ms | ✅ -75% | | CLS | 0.25 | 0.06 | ✅ -76% | | Performance | 62 | 100 | ✅ +38 points |
Business Impact
The performance improvements translated directly to business metrics:
- Conversion rate: +23%
- Bounce rate: -31%
- Average session duration: +42%
- Mobile traffic: +18% (better mobile experience)
ROI of Performance
Every 100ms of improvement in load time increased conversions by 1%. Performance optimization is not just technical – it's a business imperative.
Key Lessons
- Measure first: Use Lighthouse CI in your deployment pipeline
- Optimize images: They're usually the biggest win
- Think critical path: What does the user need to see first?
- Monitor in production: Synthetic tests don't capture real user experience
- Iterate continuously: Performance is not a one-time task
Tools We Used
- Lighthouse CI: Automated performance testing
- Chrome DevTools: Performance profiling
- WebPageTest: Real-world performance testing
- Next.js Bundle Analyzer: Bundle size analysis
- react-window: List virtualization
Want to learn more about web performance? Check out web.dev for comprehensive guides and best practices.