Skip to content
AK.
All articles
Performance10 September 202412 min read

How I Improved LCP by 40% on a Production Next.js Storefront

A real-world breakdown of every optimization applied to a high-traffic UK telecom e-commerce frontend — from route-level code splitting to font strategy and third-party script deferral.

The Starting Point

When I inherited the BT/EE storefront codebase, Lighthouse was returning a mobile LCP of 6.2s and a CLS of 0.28 — both failing Core Web Vitals thresholds. The site had accumulated years of third-party scripts, unoptimised images and a single monolithic JS bundle. Traffic was real and revenue was directly attached to page load time, so every improvement was measurable in pounds, not just percentages.

Route-Level Code Splitting

The first win came from fixing the bundle strategy. The app was shipping the entire dependency graph to every route. Using Next.js dynamic imports with proper loading boundaries, we isolated heavy components — particularly the mega-menu, product comparison table and the bundle-picker modal — behind lazy boundaries. The initial JS payload dropped by 38%. The key insight: dynamic() in Next.js is not just for convenience — it's a first-class performance primitive when you treat it as part of your rendering strategy rather than an afterthought.

typescript
// Before: imported eagerly everywhere
import { MegaMenu } from '@/components/MegaMenu';

// After: lazy with a minimal skeleton fallback
const MegaMenu = dynamic(() => import('@/components/MegaMenu'), {
  ssr: false,
  loading: () => <div className="h-12 w-full animate-pulse bg-surface" />,
});

Image Pipeline Hardening

Hero images were the primary LCP candidate — a 2MB uncompressed JPEG served to all viewports. The fix was multi-layered: Next.js Image component with explicit width/height, priority on above-the-fold images, avif/webp format negotiation, and a correctly configured sizes attribute so the browser could make an accurate network decision before downloading. We also moved to a CDN with edge-side image transforms so we could serve device-precise resolutions rather than one large image for all.

tsx
<Image
  src={heroImage}
  alt={product.name}
  width={1200}
  height={675}
  priority          // LCP candidate — don't lazy load
  quality={82}
  sizes="(max-width: 640px) 100vw, (max-width: 1200px) 75vw, 1200px"
  placeholder="blur"
  blurDataURL={product.lqip}
  className="object-cover"
/>

Font Loading Strategy

The site was loading four font families with font-display: auto, blocking render until each loaded. The fix: use font-display: swap for body fonts, preload only the weight used above-the-fold, and subset to latin characters only. For the display font (used only in headings), we switched to a variable font so one file served all weights. This alone saved 3 round trips on initial load.

Third-Party Script Deferral

Analytics, chat widgets, A/B testing scripts and tag manager containers were all in the <head> with no loading strategy. Each one blocked parsing. Using Next.js Script component with strategy='afterInteractive' for analytics and strategy='lazyOnload' for non-critical widgets moved all of them out of the critical path. The chat widget — which was contributing 380ms of main-thread blocking — was deferred until user interaction.

tsx
// Analytics loads after page becomes interactive
<Script
  src="https://www.googletagmanager.com/gtag/js?id=G-XXXX"
  strategy="afterInteractive"
/>

// Chat widget only loads when user scrolls or clicks
<Script
  src="https://cdn.example.com/chat.js"
  strategy="lazyOnload"
/>

The Results

After applying all optimisations over a 6-week sprint: LCP improved from 6.2s to 3.7s (40% reduction), CLS dropped from 0.28 to 0.04, FCP improved by 35%, and Lighthouse mobile score moved from 51 to 96. Measured in the field using Chrome UX Report data, not just lab runs. The lesson: Core Web Vitals optimisation is not a single change — it's a systematic audit of everything in your critical path and a deliberate rendering strategy for each route.

Building something similar? I'd like to hear about it.

Start a conversation