Performance · 2026-09-04 · 12 min read

Why your LCP is 4.2s even after “fixing” it

A green Lighthouse result is not proof that the page is fast for users. A 4.2s LCP after an apparent fix usually means the measured bottleneck moved: the lab image changed, but field users still wait on TTFB, font swap, hydration, or a late layout decision.

Scope: This is an implementation reference. Validate claims against current Google, vendor, and platform documentation before making a release or policy decision.

Start with the right trace

Measure the same URL in three places: Search Console/CrUX for field distribution, PageSpeed Insights for lab and field context, and a throttled Chrome trace for causality. Record device class, connection, URL, release SHA, and the LCP element. Never compare a mobile field p75 to a desktop Lighthouse score.

# Capture a repeatable local trace
npx lighthouse https://example.com/landing \
  --preset=desktop --throttling-method=simulate \
  --output=json --output-path=./artifacts/lighthouse.json

# Find the LCP element and long tasks
jq '.audits["largest-contentful-paint-element"], .audits["long-tasks"]' \
  ./artifacts/lighthouse.json
  1. Open DevTools Performance, enable Screenshots and Web Vitals, then record a cold mobile load.
  2. Inspect the LCP marker and verify whether it is an image, text block, or background.
  3. Switch to Network and sort by waterfall start time. A late font or script can delay paint even when the image is cached.
  4. Repeat with cache disabled and with a warm cache. The delta identifies cache-dependent regressions.

Next.js App Router bottlenecks

In the App Router, a page can be statically generated and still lose LCP to client hydration. Keep the hero and its critical copy in a Server Component. Move only interaction into a small client island. Avoid importing a large animation or analytics dependency from the hero tree.

// app/(en)/landing/page.tsx — keep the hero server-rendered
import Image from 'next/image'

export default function LandingPage() {
  return (
    <main>
      <section className="hero">
        <h1>Technical SEO systems that survive production</h1>
        <Image
          src="/hero.avif"
          alt=""
          width={1440}
          height={900}
          priority
          sizes="(max-width: 768px) 100vw, 720px"
        />
      </section>
      <InteractiveFilters />
    </main>
  )
}

If the LCP is text, preload only the font actually used by that text. If it is an image, `priority` and correct `sizes` matter more than adding a generic preload. Do not preload both desktop and mobile variants.

Fonts and third-party work

Unoptimised font CSS can make a text LCP appear late or cause a second layout. Use `next/font`, `display: 'swap'`, a realistic fallback, and preload only above-the-fold families. Third-party JavaScript should not be part of the critical path.

// lib/fonts.ts
import { Inter, Cairo } from 'next/font/google'

export const body = Inter({
  subsets: ['latin'],
  display: 'swap',
  preload: true,
  fallback: ['Arial', 'sans-serif'],
  variable: '--font-body',
})

// Analytics: afterInteractive; ads and chat: lazyOnload
<Script src={analyticsUrl} strategy="afterInteractive" />
<Script src={adsUrl} strategy="lazyOnload" />

In the trace, look for long tasks during the first 2.5 seconds. A 180ms consent manager, tag manager, or chat bootstrap can delay the exact frame that contains the LCP element. Remove it from the initial route, load it after interaction, or server-render the required consent shell.

Hidden layout shifts that keep LCP late

CLS and LCP often share a cause: the browser cannot settle the layout because dimensions are unknown. Reserve image, ad, embed, and font-dependent space. Do not inject a banner above existing content after the first paint.

/* Reserve media space before the request completes */
.hero-media { aspect-ratio: 16 / 10; }
.ad-slot { min-height: 280px; }

/* Never use a late-loaded background as the only hero image */
.hero img { display: block; width: 100%; height: auto; }

The final test is field verification. Compare CrUX p75 after the same URL group has enough new-user traffic. A lab fix is a hypothesis; field p75 is the release decision.