← All posts

Cutting LCP from 3.2s to 0.7s on a Next.js marketing site

The audit and the 8 changes that moved the needle.

A B2B SaaS client came to us with a marketing site that "felt fine" — Lighthouse mobile score in the low 70s, no obvious complaints from users. The catch: paid traffic was converting 2.3% on desktop and 0.8% on mobile. The desktop number was OK; the mobile number was lighting money on fire.

The first instinct in this situation is to redesign the page. Don't. Before touching the design, audit how the page delivers. Here's what we found, and what fixed it.

The audit: where the seconds were going

We ran a synthetic test on a throttled 4G connection (the closest analogue to a real Indian / SE-Asia mobile visitor) and got these numbers from WebPageTest:

  • LCP: 3.2s (poor — Google's threshold is 2.5s)
  • INP: 410ms (poor — threshold is 200ms)
  • CLS: 0.21 (poor — threshold is 0.1)
  • Total blocking time: 1.8s

The page looked done in about 1.5 seconds. But the LCP element — the hero image — wasn't visible until 3.2s because the browser was busy parsing 480KB of JavaScript before it could even fetch it. The interactive controls were locked for another 410ms after that, because a third-party chat widget kept blocking the main thread.

The 8 changes that fixed it

1. Preload the LCP image, prefer AVIF

The single largest win. We added a <link rel="preload" as="image" href="/hero.avif"> in <head>, and re-encoded the hero from a 320KB JPEG to a 48KB AVIF. LCP dropped by 1.1 seconds immediately.

2. Self-host the font, drop font-display: swap for optional

Google Fonts was injecting two render-blocking stylesheets plus a CSP-blocked WOFF2. We downloaded the WOFF2 directly, served it from /public/fonts/, and used font-display: optional — which tells the browser "if the font isn't already cached, just use the system fallback." On a return visit, the real font appears. On the first visit, the user sees text instantly.

3. Move the chat widget to requestIdleCallback

The chat script was loading synchronously and blocked the main thread for 410ms. Wrapping it in requestIdleCallback(() => loadChat()) kept the button visible (rendered as plain HTML) but delayed the heavy JS until the browser had nothing better to do.

4. Lazy-mount below-the-fold sections

Next.js bundles every React component on the route — even the testimonials carousel sitting 4000px below the hero. We swapped to next/dynamic imports with { ssr: false } on the components users wouldn't see for 6+ seconds. The initial JS payload dropped from 480KB to 220KB.

5. Reserve space for everything

CLS of 0.21 was almost entirely caused by the testimonials section, which loaded fonts late and shifted the page by 60px on font swap. Setting explicit min-height on every async section took CLS to 0.02.

6. Cache static assets at the edge

The host was returning Cache-Control: no-cache on the bundled JS. Configuring immutable caching on Vercel's edge meant repeat visitors loaded zero JS over the network.

7. Inline critical CSS, defer the rest

Next.js does this by default for App Router routes but the client was on the Pages Router. We extracted the above-the-fold styles (~3KB) into a <style> tag in <Head> and deferred the rest with media="print" trick.

8. Drop the heaviest third-party script

An analytics-stacked pixel was adding 110KB and 800ms of script evaluation. The client agreed to consolidate to GA4 + a single conversion API call.

The final numbers

  • LCP: 0.7s (from 3.2s)
  • INP: 90ms (from 410ms)
  • CLS: 0.02 (from 0.21)
  • Mobile conversion: 2.1% (from 0.8%)

The kicker: we changed zero design elements. Same hero, same copy, same testimonials. Just delivered them honestly. 2.6× the mobile conversion rate with zero ad-budget increase.

The lesson

Most marketing sites today aren't slow because the design is heavy. They're slow because the build pipeline ships everything everywhere, regardless of whether the user has scrolled to it yet. Audit, measure, then ship only what's actually visible. Performance is a designable surface — start treating it like one.