All articles
Web Performance

LCP Debugging Checklist for Modern Frontends

Failing Largest Contentful Paint? Use this practical checklist to debug TTFB, resource discovery, fetch priority, render delay, and heavyweight hero assets in modern frontends.

Share:

A team ships a modern React, Next.js, or Vue frontend. On a fast laptop with a fast connection, Lighthouse gives a reasonable score. The team moves on. Weeks later, Search Console or the organisation’s field-data dashboard reports that Largest Contentful Paint is failing for a significant share of real users.

The instinct is to blame the framework. The real cause is usually more specific: the browser cannot discover, prioritise, or render the LCP element early enough. Modern frontend stacks are fast when used with the browser’s loading model — and slow when they fight it.

Debugging LCP well means stopping the reflexive “compress the hero image” response and instead following the browser’s actual path from request to paint. That path has four distinct phases, and any one of them can be the bottleneck.

TL;DR

  • LCP should be debugged as four phases: TTFB, resource load delay, resource load duration, and element render delay.
  • The browser must be able to discover the LCP element from the initial HTML.
  • The LCP resource must never be loading="lazy".
  • Likely LCP images often need explicit priority help such as fetchpriority="high".
  • Render delay can come from blocking CSS, fonts, JavaScript, hydration timing, or late-binding client logic.
  • After discovery and prioritisation are fixed, file size still matters — but it is the last lever, not the first.

Start with a fast audit of the page, then use the checklist below to find which LCP phase is actually wasting your budget. If you want a quick external snapshot of the page’s technical signals, run the CodeAva Website Audit before you open DevTools.

The four parts of LCP you need to debug

LCP is not a single number you can improve by optimising one thing. The Chrome team decomposes it into four sequential phases, and effective debugging means figuring out which phase is eating your budget.

PhaseWhat it measuresTypical causes when it is slow
1. TTFBTime from navigation start to the first HTML byte.Slow origin, no caching, heavy server work, distance from user to edge.
2. Resource load delayTime between HTML arriving and the LCP resource starting to download.Lazy-loaded LCP element, CSR-only discovery, missing preload, low priority, late CSS/JS references.
3. Resource load durationTime the LCP resource actually spends downloading.Oversized image, wrong format, no responsive sources, uncompressed video poster, slow CDN.
4. Element render delayTime from the resource finishing to the element being painted.Render-blocking CSS/JS, slow web fonts, hydration-gated rendering, client-only content swaps.

Good LCP work means shrinking the delay phases before compressing assets blindly. A well-discovered, high-priority, reasonably sized image almost always beats a hyper-compressed one that the browser finds too late.

Step 1: identify the actual LCP element

The most common mistake in LCP work is optimising the wrong element. Developers assume the visible hero image is the LCP candidate and compress it for weeks without improvement. The actual LCP element might be a large headline, a paragraph of text, a video poster, or a completely different image from the one the team had in mind.

Confirm the candidate before changing anything:

  • Open Chrome DevTools and run a Performance recording on a cold page load. The LCP marker in the timeline labels the exact element that was measured.
  • Use the Performance Insights panel for a simpler view that highlights LCP, render-blocking resources, and critical requests.
  • Install the Web Vitals browser extension to see the live LCP candidate as you interact with the page.
  • Double-check on mobile emulation — the LCP element on mobile is often different from the one on desktop because the viewport and layout differ.

Lab tools diagnose, field data verifies

Chrome DevTools and Lighthouse are excellent for reproducing and fixing LCP issues in isolation. They do not replace real-user data. Once a fix is deployed, verify the improvement in Search Console Core Web Vitals, CrUX, or your own RUM dashboard across devices and network conditions.

Step 2: fix TTFB first if the page starts too late

TTFB is the foundation of LCP. If the first byte of HTML arrives late, every subsequent phase inherits that lateness — there is simply less budget available for discovery, load, and render.

Common causes of slow TTFB:

  • Origin-only serving with no CDN or edge cache. Every request hits the origin, adding latency for users far from the server.
  • Heavy server-side work on every request: uncached database queries, blocking third-party API calls, slow rendering of large server components.
  • Cold starts in serverless environments, especially for rarely-visited routes.
  • Long redirect chains before the final HTML response is served.

Fixes are architecture-specific: static generation or incremental regeneration where possible, edge caching for cacheable responses, HTTP caching headers that let CDNs serve repeat visits, and trimming synchronous work from the hot path. The goal is a TTFB that leaves enough of the LCP budget for the remaining three phases to actually complete in time.

TTFB is not the cause of every LCP issue, and there is no single TTFB threshold that makes good LCP impossible. Treat it as a budget tradeoff: the lower your TTFB, the more slack you have for everything that follows.

Step 3: eliminate resource load delay

Resource load delay is where most modern-frontend LCP bugs live. The HTML has arrived. The browser is ready to fetch the LCP resource. But for a long list of reasons, it does not start fetching for hundreds or thousands of milliseconds.

Cause: the LCP image is lazy-loaded

loading="lazy" tells the browser to delay fetching the image until it is near the viewport. For an above-the-fold hero image, this directly sabotages LCP.

<!-- Wrong: the LCP candidate is deferred -->
<img src="/hero.webp" alt="Hero" loading="lazy">

<!-- Right: the LCP image loads as early as the browser allows -->
<img src="/hero.webp" alt="Hero" width="1200" height="800" fetchpriority="high">

Never lazy-load the LCP element. Use loading="lazy" only for content safely below the fold. Also include explicit width and heightattributes so the browser can reserve layout space before the image arrives — this helps CLS and helps some render-delay cases too.

Cause: the image is only discoverable after JavaScript runs

If the LCP image is inserted into the DOM by client-side JavaScript after hydration, the browser’s preload scanner never sees it. The fetch cannot begin in parallel with the HTML stream — it waits for the JS bundle to download, parse, and execute.

Fixes:

  • Server-render or statically generate the LCP markup so it appears in the initial HTML.
  • If the image must come from client logic, add a <link rel="preload"> hint in the document <head> so the preload scanner can find it before the framework boots.
<!-- When the LCP image is injected by client-side JS, preload it -->
<link
  rel="preload"
  as="image"
  href="/hero.webp"
  fetchpriority="high"
  imagesrcset="/hero-800.webp 800w, /hero-1600.webp 1600w"
  imagesizes="100vw"
>

Preloads are a last resort, not a default. When the LCP image is already a plain <img> in server-rendered HTML, fetchpriority="high" is usually the cheaper, correct signal.

Cause: the image is a CSS background

CSS background images are discovered only after the relevant CSS has been parsed and matched, which often means after render-blocking stylesheets have loaded and applied. If your LCP candidate is a CSS background, switch it to a real <img> element when you can. If you must keep it as a background, preload it explicitly.

Cause: low priority because the browser guessed wrong

By default, images are not given the highest priority. If the LCP image is competing with scripts, stylesheets, and other images, the browser may queue it behind them. fetchpriority="high" moves a specific image to the front of the queue. Use it sparingly — only on the one image that is the LCP candidate.

Do not lazy-load the LCP element

Applying loading="lazy" to the LCP image is one of the most common and most destructive LCP regressions. Automated tools, CMS defaults, and framework configurations sometimes add it globally. Verify the LCP element does not inherit a lazy-loading attribute from any of them.

Step 4: shorten resource load duration

Only after discovery and prioritisation are correct does file size become the dominant variable. At that point, the usual levers apply:

  • Use a modern format. WebP and AVIF typically produce far smaller files than JPEG and PNG at equivalent quality. For a practical comparison, see WebP vs. AVIF vs. PNG vs. JPEG.
  • Serve responsive sources. Use srcset and sizes so phones download mobile-sized images, not desktop-sized ones.
  • Compress appropriately. Do not ship a 2 MB hero image when a 200 KB WebP is visually indistinguishable.
  • Pre-size the asset to its rendered dimensions. Scaling down a huge image in CSS wastes bytes and decode time.

You can compress and convert locally with the CodeAva Image Optimizer, which converts to WebP and AVIF in the browser, supports bulk processing, and never uploads the files to a server.

Network variables matter too. A fast CDN with edge presence close to your users directly reduces load duration. HTTP/2 or HTTP/3 reduces connection overhead on cross-origin resources. Persistent connection reuse on the same origin keeps the LCP request cheap.

Step 5: close the element render delay gap

The final phase is the gap between the resource finishing and the element being painted. If this is your dominant delay, the problem is not the resource itself — it is what the browser is waiting on before it can paint.

Render-blocking CSS

The browser will not paint until render-blocking stylesheets are fetched and applied. Inline critical CSS for above-the-fold content, defer non-critical styles, and keep stylesheet count and size disciplined.

Web fonts

If the LCP element is text that depends on a web font, paint may be blocked until the font loads. Use font-display: swap or optional to let the browser show a fallback font immediately. Preload the primary font file if it is essential for the above-the-fold paint.

/* Avoid invisible-text until the web font loads */
@font-face {
  font-family: "AppSans";
  src: url("/fonts/AppSans.woff2") format("woff2");
  font-display: swap;
}

JavaScript and hydration

Long tasks from large JavaScript bundles delay the main thread and can block paint commits. In frameworks that render on the client or defer content until hydration, the LCP element might exist in the DOM but paint only after a heavy hydration pass.

  • Ship less JavaScript on the critical path. Split, defer, and lazy-load what is not needed for the first paint.
  • Use server components, partial hydration, or islands architecture where your framework supports it, so the LCP element is not gated on a client bundle.
  • Keep third-party scripts off the critical path. Analytics, tag managers, and chat widgets routinely delay paint more than the LCP image itself.

For a deeper look at JavaScript-heavy pages and their interactivity problems, see Finding Real INP Bottlenecks in Production. The same long tasks that hurt INP often hurt LCP’s render delay phase.

Discovery and prioritisation vs. asset heaviness

One of the most useful mental separations in LCP work is the difference between discovery / prioritisation problems and asset heaviness problems. They require different fixes and are often confused for each other.

Problem typeSignalFix
Discovery / prioritisationLong gap between HTML arriving and the LCP request starting. Request begins late in the waterfall.Remove lazy-loading, add fetchpriority, server-render the markup, preload when necessary, eliminate CSS/JS dependency on the reference.
Asset heavinessLCP request starts early, but the download itself is the long bar in the waterfall.Convert to modern formats, compress, serve responsive sources, size to actual rendered dimensions, use a faster CDN.
Render delayLCP request finishes quickly, but paint happens much later.Reduce render-blocking CSS, tune font loading, split JavaScript, avoid hydration-gated content for the LCP element.

Look at the Network panel’s waterfall with the LCP marker highlighted. The shape of the waterfall tells you which column above applies.

The LCP debugging workflow

Work through the steps below in order. Each one eliminates a class of suspects so the real bottleneck surfaces faster.

  1. Identify the actual LCP element on both mobile and desktop using DevTools or the Web Vitals extension. Do not guess.
  2. Measure TTFB. If it is consistently high, address origin, caching, and edge delivery before touching frontend code.
  3. Inspect the waterfall for resource load delay. A long gap between HTML arrival and the LCP request means a discovery or priority problem.
  4. Check for loading="lazy" on the LCP image and in any image-component default that might apply globally.
  5. Ensure the LCP markup is in server-rendered HTML whenever possible so the preload scanner can find it.
  6. Add fetchpriority="high" to the LCP image if the browser is not already prioritising it.
  7. Preload only when needed— when the image is injected by client-side JS or sourced from CSS.
  8. Measure resource load duration. If the download itself is the long bar, move to format, compression, and responsive source fixes.
  9. Inspect element render delay. If the resource finishes early but paint is late, audit render-blocking CSS, fonts, and critical-path JavaScript.
  10. Verify in field data after each change. Lab fixes sometimes do not translate to real-user improvement uniformly across devices and networks.

Framework-specific pitfalls

Modern frameworks give you tools that help LCP and defaults that can hurt it. The most common regressions:

  • Image components with global lazy defaults. Some image components default to loading="lazy" everywhere. Override explicitly on the LCP element and mark it as priority.
  • Client-only rendering of the hero section.If the hero is wrapped in a client-only block, the LCP element won’t be in the initial HTML. Move it to the server render path or preload the resource.
  • Fonts configured without display: swap. The default behaviour in some font loaders hides text until the font is ready, directly inflating render delay.
  • Large CSS-in-JS bundles that are render-blocking for above-the-fold content. Inline the critical styles needed for the LCP element.
  • Waterfall-inducing data loading, where the LCP content depends on a client fetch that depends on hydration that depends on a big JS bundle. Move the data and markup to the server wherever the framework allows.

How to verify the fix landed

Lab tooling proves the fix works in a controlled environment. Field data proves it works for real users.

  • Chrome DevTools Performance panel: rerun the trace, confirm the LCP marker has moved earlier and the waterfall no longer shows the previous bottleneck.
  • Lighthouse: run before and after on the same profile and network preset to compare.
  • Search Console Core Web Vitals: monitor the URL group over the following weeks. CrUX reports on a 28-day rolling window, so changes take time to appear.
  • RUM: if you collect real-user metrics, watch the p75 LCP for the affected pages.

If the page still fails after discovery, prioritisation, load duration, and render delay are all addressed, the bottleneck is usually architectural — TTFB, hydration-gated rendering, or a framework pattern that does not match the browser’s loading model. Step back to the four-phase table and re-identify which phase is actually dominant before changing more code.

LCP is a pipeline, not a single number

Every LCP regression has a specific phase it lives in. The teams that fix LCP reliably are the ones that stop compressing random images and start diagnosing which of the four phases — TTFB, resource load delay, resource load duration, or element render delay — is consuming the budget on a given page.

Start with identification, fix discovery and prioritisation before asset size, and verify with real-user data. The framework is usually not the problem. The mismatch between framework defaults and the browser’s loading model is.

When you are ready to audit a specific page, start with the CodeAva Website Audit for a quick external snapshot, compress and convert hero assets locally with the CodeAva Image Optimizer, and if interactivity metrics look equally rough, read Finding Real INP Bottlenecks in Production to handle the main-thread side of the performance story.

#LCP#core-web-vitals#largest-contentful-paint#performance#react#nextjs#fetchpriority#preload#rendering#frontend-debugging

Frequently asked questions

More from Sophia DuToit

Found this useful?

Share:

Want to audit your own project?

These articles are written by the same engineers who built CodeAva\u2019s audit engine.