Gurkirt Heerey

Largest Contentful Paint: The Four Phases That Make or Break Your Load Time

performanceweb-vitalsnext.js

LCP is a Google Core Web Vital that measures perceived page load time by tracking when the largest visual element (image, video, or block element) renders within the viewport. The target is under 2.5 seconds.

Most people treat LCP as a single number to optimize. But every LCP score is actually the sum of four sequential phases, and knowing which phase is slow completely changes how you fix it.

The four phases

1. TTFB (Time to First Byte)

How long until the server responds with the first byte of HTML. If this is slow, everything downstream is slow.

  • Fix with: edge rendering, CDN, server caching, faster DB queries, streaming SSR
  • If TTFB is over 500ms, audit what the server is doing before responding

2. Resource Load Delay

Time between TTFB and the browser discovering the LCP resource. The browser can't load a hero image if it's buried in CSS or loaded by JavaScript.

  • If the <img> tag is in the initial HTML (via SSR), the browser's preload scanner finds it during HTML parsing, skipping this delay entirely
  • Render-blocking CSS and JS are the main killers here

3. Resource Load Time

How long the LCP resource takes to download.

  • Fix with: image compression, modern formats (WebP/AVIF), responsive images (srcset), CDN
  • next/image handles format conversion, resizing, and CDN caching automatically via Vercel's image optimization

4. Element Render Delay

Time between the resource downloading and actually painting on screen. Usually caused by JS execution or unfinished CSS loading.

What this looks like in practice

Say you have a page with a hero image and the LCP clocks in at 2 seconds:

PhaseTimeProblem
TTFB800msNext.js SSR hitting Supabase, waiting for data
Resource Load Delay600msHero image in a React component, needs JS bundle to parse before browser discovers it
Resource Load Time400ms500KB uncompressed PNG (WebP would be ~100KB, ~100ms)
Element Render Delay200msRender-blocking CSS still loading
Total LCP2000ms

The biggest win here? If the <img> was in the initial SSR HTML, the preload scanner would skip the 600ms resource load delay entirely. That's 30% of the total LCP gone by changing where the image is discovered, not how it's served.

Font loading and LCP

Loading fonts from Google Fonts hits three domains (your site, fonts.googleapis.com, fonts.gstatic.com), each requiring DNS lookup, TCP connection, and TLS handshake. That's 200-400ms per domain on slow connections.

Self-hosting eliminates the extra connections: download the .woff2 file, put it in /public/fonts/, reference it in a @font-face rule, and add a preload tag.

In Next.js, next/font does all of this at build time automatically:

import { Inter } from 'next/font/google'
const inter = Inter({ subsets: ['latin'] })

No external requests at runtime. The font file gets inlined and self-hosted.

Image loading behavior

next/image applies loading="lazy" by default unless you set priority.

<!-- loads when near viewport -->
<img src="/hero.png" loading="lazy" />

<!-- loads immediately -->
<img src="/hero.png" loading="eager" />

<!-- default browser behavior: same as eager -->
<img src="/hero.png" />

For LCP images, always set priority (or loading="eager") so the browser loads them immediately. Lazy loading your LCP image is one of the most common performance mistakes, and it's easy to miss because it looks like you're doing the right thing.