# How I Cut LCP from 3.5s to 1.2s on a Next.js E-Commerce Site
By Govind Bajaj · 2025

Tags: next.js, core-web-vitals, lcp, performance, web-performance
Lighthouse was at 65. LCP sat at 3.5 seconds. Two weeks of focused Core Web Vitals work brought Lighthouse to 95 and LCP to 1.2s. Here's the exact checklist.
The Sans Herbals homepage had a Lighthouse score of 65 and an LCP of 3.5 seconds when I ran the first audit.

The hero image was a 2.1MB JPEG. The product thumbnails were unoptimized PNGs. The custom font loaded via @import blocked rendering for 800ms. And the analytics script loaded synchronously in the head.

Two weeks later: Lighthouse 95, LCP 1.2 seconds. No redesign, no feature cuts. Just systematic performance optimization. Here is the exact checklist I used.

## What LCP Actually Measures

Largest Contentful Paint tracks how long the largest visible element takes to render. For most e-commerce sites, this is the hero image or the main product image. Google wants it under 2.5 seconds.

In 2025, only 62% of mobile sites meet this threshold. The rest are leaving money on the table — slow LCP correlates with 8-35% worse conversion rates depending on the study.

## Step 1: Fix the Hero Image (Biggest Impact)

The hero image was 2.1MB, 3840px wide, served as a JPEG. The viewport is 1440px wide on desktop, 375px on mobile.

```tsx
// Before: unoptimized img tag
<img src="/hero.jpg" alt="Herbal products" />

// After: Next.js Image component with sizing
import Image from 'next/image'

<Image
  src="/hero.jpg"
  alt="Herbal products"
  width={1440}
  height={600}
  priority        // preload this image
  quality={80}    // optimal quality/size tradeoff
  sizes="100vw" // responsive sizing hint
/>
```

The Next.js Image component automatically converts to WebP (30% smaller than JPEG), generates responsive sizes for different viewports, adds lazy loading (except with priority), and prevents layout shift by reserving space.

Result: 2.1MB to 87KB WebP. LCP improved by 1.4 seconds.

## Step 2: Preload Critical Resources

The browser discovers resources by parsing HTML. For below-the-fold images, this is fine. For the LCP element, you want the browser to start loading as early as possible.

```tsx
// app/layout.tsx
import { preload } from 'react-dom'

export default function RootLayout({ children }) {
  preload('/hero.jpg', { as: 'image' })
  
  return (
    <html>
      <head>
        <link rel="preload" href="/hero.jpg" as="image" type="image/webp" />
      </head>
      <body>{children}</body>
    </html>
  )
}
```

With priority on the Next.js Image component, this happens automatically. But for custom elements or fonts, manual preloading matters.

## Step 3: Fix Font Loading

The custom font loaded via CSS @import, which blocks rendering until the font file downloads.

```css
/* After: font-display swap with preload */
@font-face {
  font-family: 'Inter';
  src: url('/fonts/Inter-var.woff2') format('woff2');
  font-weight: 100 900;
  font-display: swap; /* Show fallback font immediately, swap when loaded */
}
```

font-display: swap prevents invisible text during font loading. The user sees content immediately with a fallback font, then the custom font replaces it when loaded. This eliminates the font blocking time from LCP.

## Step 4: Defer Non-Critical Scripts

Third-party scripts are performance killers. Analytics, chat widgets, cookie banners — they all compete for bandwidth and CPU during initial load.

```tsx
// After: Next.js Script component with lazy loading
import Script from 'next/script'

<Script
  src="https://analytics.example.com/script.js"
  strategy="lazyOnload"  // Load after everything else is interactive
/>
```

The afterInteractive strategy loads the script after the page becomes interactive. lazyOnload waits until the load event fires. For analytics, afterInteractive is usually the right balance — you get tracking without blocking rendering.

## Step 5: Use React Server Components for Static Content

Client Components ship JavaScript to the browser. Server Components do not. For a homepage that is mostly static content with a few interactive elements, this matters.

```tsx
// Server Component — zero JS shipped to client
export default async function Homepage() {
  const products = await getFeaturedProducts()
  
  return (
    <div>
      <HeroSection />
      <ProductGrid products={products} />
      {/* Only the AddToCartButton is a Client Component */}
      <AddToCartButton />
    </div>
  )
}
```

The ProductGrid renders on the server, sending HTML only. The AddToCartButton is the only Client Component, so it is the only JavaScript shipped for interactivity.

## Step 6: Enable Compression

Ensure your server or CDN serves assets with Brotli or Gzip compression. Most hosting platforms do this by default, but verify it:

```bash
curl -H "Accept-Encoding: br" -I https://yoursite.com
# Check for: content-encoding: br
```

Brotli typically achieves 20-26% better compression than Gzip for text assets.

## The Results

MetricBeforeAfterChangeLighthouse6595+30LCP3.5s1.2s-2.3sHero image size2.1MB87KB-96%Total page weight4.8MB1.1MB-77%Time to Interactive5.2s2.1s-3.1s

## Takeaways

- LCP is usually determined by one element — find it and optimize it ruthlessly
- Next.js Image component with priority is the single biggest LCP win for image-heavy sites
- font-display: swap eliminates font loading from the LCP calculation
- Defer third-party scripts with Next.js Script component strategies
- Server Components reduce JavaScript payload, improving both LCP and TTI
- Run Lighthouse in incognito mode with CPU throttling for realistic results