Back to Portfolio
PerformanceNext.jsCloudflareOptimization

How I Improved Lighthouse Score from 65 to 95 — A Real Case Study

May 2026 10 min readBy Shivaji Tiwari
65
Before — Poor
95
After — Excellent
Platform: Cuisinao — 10,000+ monthly users, 5,000+ content records

Step 1: Diagnose Before Fixing

The first mistake people make with performance optimization is jumping straight to fixes without measuring. Run Lighthouse in Chrome DevTools on a production build (not dev mode — dev builds are always slow). Note the specific metrics: LCP, FID/INP, CLS, and TBT. These tell you where to focus.

On this project, the main issues were: LCP of 6.2s (target: under 2.5s), a 4.2MB JavaScript bundle, and unoptimized images averaging 800KB each. Those three issues caused 90% of the score gap.

Step 2: Fix the JavaScript Bundle

A 4.2MB bundle on first load is the #1 killer of performance. The fix: code splitting and lazy loading. In Next.js, dynamic() imports defer loading of components until they're needed:

import dynamic from 'next/dynamic';

// Heavy chart library — only loads when the analytics page is visited
const AnalyticsChart = dynamic(() => import('@/components/AnalyticsChart'), {
  loading: () => <ChartSkeleton />,
  ssr: false,
});

We moved from one 4.2MB bundle to a 280KB initial bundle with lazy-loaded chunks. This alone dropped LCP by 2.8 seconds.

Step 3: Image Optimization with Cloudflare R2

The platform had 5,000+ recipe images stored on S3, served as raw JPEGs. We migrated to Cloudflare R2 with automatic WebP conversion and responsive sizing. The setup:

  • Upload originals to R2 (cheaper than S3 egress costs)
  • Cloudflare Image Resizing serves WebP at the requested dimensions
  • Next.js Image component with sizes prop requests the right size per breakpoint
  • Images cached at Cloudflare's edge — near-instant delivery globally

Average image size dropped from 800KB to 45KB. Media load time reduced by 40%.

Step 4: SSG for Content Pages

Recipe pages are read-heavy and change infrequently. Switching from SSR (rendered on every request) to ISR (Incremental Static Regeneration — rendered once, cached, re-generated every hour) eliminated server rendering time from the LCP calculation. The page was served as a static HTML file from Cloudflare's cache.

// Next.js 14 App Router ISR
export const revalidate = 3600; // re-generate every hour

Step 5: Fix Layout Shift (CLS)

CLS (Cumulative Layout Shift) was 0.18 — above the 0.1 target. Two causes: images without explicit dimensions (browser doesn't reserve space) and web fonts loading after text renders. Fixes: always set width and height on images, and use font-display: optional for non-critical fonts. CLS dropped to 0.02.

The Results

  • Lighthouse Performance: 65 → 95
  • LCP: 6.2s → 1.4s
  • Bundle size: 4.2MB → 280KB initial
  • Image load time: 800KB avg → 45KB avg (−94%)
  • Media load time overall: −40%
Need Your Platform Optimized? →