LEARN · DEBUGGING GUIDE

LCP Slow: How to Diagnose and Fix Largest Contentful Paint Issues

LCP measures when the largest content element becomes visible. Slow LCP kills user experience and SEO. Here's how to find the bottleneck and squash it.

IntermediatePerformance8 min read

What this usually means

LCP is the time from page start until the largest image or text block is painted. A slow LCP (>2.5s) usually means one of: slow server response (TTFB > 1.5s), render-blocking JavaScript/CSS delaying first paint, slow resource load (hero image not optimized, no preload, or poor compression), or client-side rendering where React/Vue/Angular needs to hydrate before painting the LCP element. Less obvious: third-party scripts that delay main-thread work, or font loading that causes invisible text (FOIT) until the font is ready.

( 01 )Fast diagnosis

The first ten minutes — establish facts before touching code.

  • 1Run `lighthouse --view --preset=desktop` on your staging URL—check LCP score and diagnostics
  • 2In Chrome DevTools Performance tab, record a load trace and filter on 'LCP' in the summary panel
  • 3Check CrUX report in PageSpeed Insights for real-user LCP distribution (p75, p95)
  • 4Look at the Network tab: find the LCP element request (image/font) and check its start time, TTFB, and download duration
  • 5In DevTools, go to Settings > Experiments > enable 'Show Largest Contentful Paint' in the Performance panel for a visual marker
( 02 )Where to look

The specific files, logs, configs, and dashboards that usually own this bug.

  • searchChrome DevTools Performance tab—LCP marker in the timings track
  • searchLighthouse report > 'Largest Contentful Paint' diagnostics section
  • searchCrUX dashboard (crux.run) or PageSpeed Insights real-user data
  • searchServer response logs (TTFB) via `curl -w "@curl-format.txt" -o /dev/null https://yoursite.com`
  • searchNetwork tab—specifically the LCP resource's initiator and timing breakdown
  • searchYour CDN or origin server access logs for cache hit/miss on the LCP asset
  • searchGoogle Search Console 'Core Web Vitals' report for URL-level LCP data
( 03 )Common root causes

Practical causes, not theory. These are the things you will actually find.

  • warningTTFB > 1.5s due to slow backend, poor hosting, or missing CDN
  • warningRender-blocking JavaScript/CSS that delays first paint (especially unused code)
  • warningHero image not preloaded, oversized, or not using modern formats (WebP/AVIF)
  • warningClient-side rendering (CSR) where JavaScript must download, parse, and execute before painting LCP
  • warningThird-party scripts (analytics, ads, widgets) that occupy the main thread during critical rendering
  • warningFont loading causing invisible text (FOIT) until the font file is downloaded (even with font-display: swap, if fallback is too different)
  • warningServer-side rendering (SSR) that doesn't stream or sends a large HTML payload
( 04 )Fix patterns

Concrete fix directions. Pick the one that matches your root cause.

  • buildImprove TTFB: add a CDN (Cloudflare, Fastly), optimize backend queries, enable HTTP/2, use server-side caching (Redis, Varnish)
  • buildPreload the LCP image: add `<link rel="preload" href="hero.webp" as="image">` in the `<head>`
  • buildDefer non-critical CSS/JS: use `media="print"` onload-hack for CSS, `async`/`defer` for JS
  • buildOptimize images: compress to 70-80% quality, use WebP/AVIF, responsive srcset, lazy-load below-fold images only
  • buildFor CSR apps: move LCP element to server-rendered HTML (SSR/SSG), or use streaming SSR (React 18, Vue 3)
  • buildEliminate render-blocking third-party scripts: load them after LCP via `async` or move to service worker
  • buildImplement font-display: swap with a fallback font that has similar metrics to reduce layout shift
( 05 )How to verify

A fix you cannot prove is a guess. Close the loop.

  • verifiedRun Lighthouse again and confirm LCP < 2.5s for both mobile and desktop
  • verifiedUse WebPageTest (webpagetest.org) with a representative connection (3G Slow) and check filmstrip
  • verifiedMonitor real-user LCP via RUM (CrUX, New Relic, Datadog) for at least 7 days post-deploy
  • verifiedCheck that the LCP resource now starts loading early (request waterfall shows preload in the initial request)
  • verifiedVerify TTFB is < 800ms using `curl` or DevTools network tab on cold cache (first visit)
( 06 )Mistakes to avoid

Things that make this bug worse or harder to find.

  • warningPreloading every image—only preload the LCP element (preload pushes other resources down)
  • warningUsing `fetchpriority="high"` on the LCP image without also preloading—priority hints are not enough alone
  • warningFocusing only on images: text LCP can be just as slow if fonts block rendering
  • warningIgnoring the impact of cookie-consent banners or A/B testing scripts that inject CSS/JS early
  • warningOptimizing for a single device—test on a real mid-range mobile (Moto G4) with throttled CPU
  • warningMaking changes without measuring before/after with the same conditions (no A/B testing on RUM)
( 07 )War story

The 4.2s LCP That Wasn't the Image

Senior Frontend EngineerNext.js (SSR), Cloudflare CDN, Google Fonts, custom analytics script

Timeline

  1. 09:15Alerts: CrUX LCP increased from 1.8s to 4.2s for /product pages over 2 hours.
  2. 09:20Checked Lighthouse—LCP 4.5s, TTFB 200ms, image loaded within 1s. Not the image.
  3. 09:30Performance trace showed long gaps between HTML and paint—main thread blocked by third-party analytics script.
  4. 09:40Analytics script loaded synchronously in <head>. Changed to `async` and added `fetchpriority="low"`.
  5. 09:50Deployed fix to staging—Lighthouse LCP dropped to 1.9s. Tested on real device—okay.
  6. 10:00Pushed to production and monitored CrUX. LCP returned to ~2.0s after 24 hours.
  7. 10:15Post-mortem: no regression detected. Root cause: a new A/B test script added to head synchronously.

I got paged at 9:15 AM. CrUX showed LCP for our product pages spiking from a steady 1.8s to 4.2s in the last two hours. My first instinct was 'hero image too big'—we'd just deployed a new banner. I opened Lighthouse on the product page. TTFB was 200ms—great. The hero image (a WebP, 80KB) loaded in 800ms. But LCP was 4.5s. That didn't add up.

I opened the Performance tab and recorded a load trace. The LCP marker appeared at 4.5s, but the image finished at 1.2s. Something else was delaying paint. I clicked on the main thread and saw a long task running from 1.5s to 3.8s—our analytics provider's script. It was included synchronously in the <head> via a <script> tag without async or defer. That script was blocking the main thread, preventing React hydration and ultimately the page from rendering.

I moved the analytics script to an async load and added `fetchpriority="low"` to deprioritize it. Deployed to staging—LCP dropped to 1.9s. We pushed to production and monitored CrUX. After 24 hours, LCP was back to ~2.0s. The post-mortem revealed that a marketing colleague had added an A/B test snippet earlier that day, also synchronous. We added a lint rule to ban synchronous third-party scripts in <head>. Lesson: always profile the main thread before blaming images.

Root cause

Synchronous third-party analytics script in <head> blocking the main thread for 2.3 seconds, delaying React hydration and paint.

The fix

Loaded all third-party scripts asynchronously with `async` and `fetchpriority="low"`, and added a CI lint rule to prevent synchronous scripts in <head>.

The lesson

LCP is not always about the largest element's load time—check what's blocking the main thread around the paint time. Profile the Performance trace, not just the network tab.

( 08 )Understanding LCP: The Real Metrics Behind the Paint

LCP is defined as the render time of the largest image or text block visible in the viewport, relative to when the page first starts loading. The metric captures perceived load speed. A score is 'good' if < 2.5s, 'needs improvement' between 2.5s and 4.0s, and 'poor' above 4.0s. But the raw number hides the breakdown: TTFB, resource load delay, and render delay.

The LCP element can change during load—a new larger element may appear. Chrome DevTools now highlights the final LCP element with a green rectangle in the Performance tab. You can also run `performance.getEntriesByType('largest-contentful-paint')` in the console to see the element and its time. The key sub-phases: TTFB (time to first byte), resource load (if the LCP is an image), and element render delay (time from resource finish to paint). The element render delay is often where main-thread blocking happens.

( 09 )Non-Obvious Causes: Third-Party Scripts and Fonts

The most common hidden cause is third-party scripts. Even `async` scripts can delay LCP if they execute during the critical path and cause layout or paint work. Use the Performance tab's 'Long Tasks' view: any task > 50ms that overlaps with the LCP timeline is suspect. Also check if the script modifies the DOM (e.g., injecting a banner) that triggers a recalculation. For fonts, `font-display: swap` is not a silver bullet—if the fallback font has a different size, the browser may re-layout when the real font loads, causing a new LCP candidate to appear (and resetting the LCP time). Always use `font-display: optional` if you want to avoid invisible text entirely, or use a fallback with similar metrics (use `size-adjust` in @font-face).

Another non-obvious cause: the LCP element is a text block that is rendered by a client-side framework after hydration. Even with SSR, if the HTML contains placeholder text that is replaced by JavaScript, the LCP may be the placeholder text that gets painted early, but then replaced by the real text—however, LCP only measures the first paint of the largest element, so if the placeholder is smaller, the real text appears later. The fix is to server-render the actual content without client-side replacement, or use streaming SSR to push the real content early.

( 10 )Tools and Commands for LCP Debugging

Chrome DevTools: Performance tab > 'LCP' marker. Also enable 'Show Largest Contentful Paint' in Settings > Experiments. WebPageTest: run a test and look at the 'Largest Contentful Paint' tab—it shows the exact element and a filmstrip. For RUM, use `performanceObserver` to capture LCP entries and send them to your analytics. Example snippet: `new PerformanceObserver((list) => { for (const entry of list.getEntries()) { console.log(entry.startTime, entry.element); } }).observe({type: 'largest-contentful-paint', buffered: true});`.

For server-side debugging, measure TTFB with `curl -o /dev/null -s -w 'time_pretransfer:%{time_pretransfer} time_starttransfer:%{time_starttransfer} total:%{time_total}' https://yoursite.com`. If TTFB > 800ms, investigate backend or CDN. Also check if your CDN is caching the HTML: a miss can cause a slow origin response. Use `curl -I` to inspect cache headers like `cf-cache-status` (Cloudflare) or `x-cache` (Fastly).

( 11 )Advanced Fix: Stream and Preload the LCP Element

For SSR apps, streaming the HTML can push the LCP element to the browser before the full page is ready. In Next.js, enable streaming with `experimental: { streaming: true }` (Next 13+) or use React 18's `renderToPipeableStream`. The key is to flush the head early, then the body containing the LCP element, while the rest of the page streams. This can cut LCP by 30-50%.

Preloading the LCP image is straightforward but often misused. Only preload the image that will be the LCP (usually the first large image above the fold). Avoid preloading multiple images—it competes with the LCP for bandwidth. Use `<link rel="preload" as="image" href="hero.webp" imagesrcset="..." imagesizes="...">` for responsive images. Also set `fetchpriority="high"` on the img tag as a hint (but preload is more reliable). For text LCP, preload the font file with `crossorigin` attribute.

( 12 )From Good to Great: Continuous LCP Monitoring

Once LCP is under control, set up monitoring to catch regressions. Use Lighthouse CI in your CI/CD pipeline to fail builds if LCP exceeds a threshold (e.g., 2.5s on a simulated mobile device). For real-user monitoring, CrUX is free but has a 28-day delay. Consider a RUM tool like SpeedCurve or Datadog RUM that surfaces LCP per URL and segment (device, browser, geography). Create alerts for when the 75th percentile LCP increases by more than 10% over a 7-day period.

Also track the sub-metrics: TTFB, resource load time, and element render delay. A tool like Web Vitals Library can report these via the `attribution` build. If you see high element render delay, it's usually main-thread blocking—profile with the Performance tab and audit your JavaScript bundles. Consider code splitting and lazy loading non-critical code.

Frequently asked questions

What if my LCP element is an image but it's not the hero image?

The LCP element is the largest visible element at the time it is painted. It could be a large heading or a carousel image. Use DevTools Performance tab to identify the exact element. If it's an unexpected element, consider making it smaller or lazy-loading bigger elements below the fold.

Does `loading=lazy` on the hero image hurt LCP?

Yes. Never use `loading=lazy` on above-fold images that could be the LCP element. Lazy loading delays the image request until it's near the viewport, which can push LCP beyond 2.5s. Use `loading=eager` (default) or `<link rel=preload>` to start the load as early as possible.

How do I debug LCP on a mobile device?

Use Chrome DevTools device emulation with a mid-range device preset (e.g., Moto G4) and network throttling (Slow 3G). For real device testing, use WebPageTest's mobile agent or connect your phone via USB and use remote debugging. The Performance tab works the same way on mobile.

Can server-side rendering alone fix LCP?

SSR helps by sending HTML with the LCP element already present, so the browser can paint it without waiting for JavaScript. However, if your SSR is slow (high TTFB) or you have render-blocking resources, LCP can still be poor. Streaming SSR provides even better results by flushing the LCP element early.

What is the element render delay and why is it important?

Element render delay is the time from when the LCP resource finishes loading to when it is painted. High render delay means the browser is busy with other tasks (JavaScript execution, style calculation, layout). Reducing main-thread work (deferring scripts, eliminating long tasks) directly reduces this delay.