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- Open DevTools Performance, enable Screenshots and Web Vitals, then record a cold mobile load.
- Inspect the LCP marker and verify whether it is an image, text block, or background.
- Switch to Network and sort by waterfall start time. A late font or script can delay paint even when the image is cached.
- 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.