← All posts

LCP Optimization: Why Your Largest Element Loads Late

HERO IMAGE ← LCP ELEMENT → WATERFALL HTML CSS JS hero-img.jpg 2.5s fonts / third-party LCP goal < 2.5s

You ran your Lighthouse audit, got a 2.8-second LCP, and the first fix that comes to mind is better hosting or a CDN. Your server is already responding in 180ms. The problem isn’t there.

LCP is frequently misdiagnosed as a server-speed problem because the metric name — Largest Contentful Paint — sounds like it’s about how fast the server is. It’s not. It’s about how long it takes for the browser to discover, fetch, and paint a single element after the initial HTML response arrives. Every step in that chain is a separate opportunity to lose time, and server speed is only the first one.

The Four Sub-Parts of LCP

Google’s own LCP attribution model breaks the metric into four sequential phases, each with a distinct cause and fix:

Time to First Byte (TTFB). The network time from the request leaving the browser until the first byte of the HTML document arrives. Fixes here are server-side: response time, CDN caching, geo-distribution. The target is under 800ms at p75, but most production sites with any CDN coverage are already below this. TTFB being slow is rarely why modern pages have LCP problems.

Resource load delay. The gap between TTFB and when the browser actually begins fetching the LCP resource. This is where most pages lose time. The browser must receive the HTML, parse it, build the render tree, and determine that a particular image or element is the largest in the viewport before it will queue the resource fetch. If the LCP image is in a <picture> element deep in the DOM, referenced by CSS background-image, or loaded via JavaScript, the browser discovers it late. The browser also won’t fetch a background image until CSSOM is constructed, which means render-blocking stylesheets add directly to this number.

Resource load time. How long the actual image or font transfer takes once the browser starts fetching it. This is where image format and file size matter. Transferring a 600KB JPEG hero image over a mobile connection with 5 Mbps throughput costs 960ms. The same hero as a well-compressed AVIF at 80KB costs 128ms. This sub-part is also affected by HTTP/3 multiplexing and the absence of head-of-line blocking — if you’re still on HTTP/1.1, you’re leaving performance on the table.

Element render delay. The gap between the LCP resource finishing its download and the element actually being painted on screen. This is caused by main-thread blocking after the resource loads: long JavaScript tasks, synchronous layout, or CSS animation forcing reflow. If your page has a large JavaScript bundle that executes after DOMContentLoaded, the main thread may be busy when the LCP image finishes downloading, pushing out paint.

Profiling LCP without sub-part attribution is guesswork. The Chrome DevTools Performance panel and the PerformanceObserver API both expose which phase is responsible for the bulk of LCP time.

The Most Common Culprits

Lazy-loading the LCP image. This is the single most common LCP mistake, and it’s entirely self-inflicted. loading="lazy" tells the browser to defer the image fetch until the element approaches the viewport. For above-fold images, the element is already in the viewport, so the browser has to wait until layout to realize it should have started fetching earlier. The result is a resource load delay of 200–800ms depending on how quickly the browser completes initial layout. The fix is trivially simple: loading="lazy" should never appear on an image that could be the LCP element. For hero images, add loading="eager" or omit the attribute entirely.

LCP image not preloaded. Even without lazy loading, images inside <picture> elements with srcset, CSS background images, and images dynamically rendered by JavaScript are discovered late. A <link rel="preload" as="image" href="..." fetchpriority="high"> in the document <head> kicks off the fetch before the parser reaches the image tag. For responsive images with srcset, use imagesrcset and imagesizes on the preload link. Without preload, discovery delay on a typical page adds 300–700ms to resource load delay.

Render-blocking stylesheets. Any <link rel="stylesheet"> in <head> blocks rendering until the stylesheet is downloaded and parsed. This increases element render delay and delays CSSOM construction, which in turn delays resource load delay for any CSS background images. The fixes are well-known but often not followed: inline critical CSS, load non-critical stylesheets asynchronously, and eliminate @import in CSS files (each @import creates a chain of serial requests).

Client-side rendered heroes. Single-page applications and JavaScript-heavy pages often render the hero element — and therefore the LCP element — via JavaScript after the initial HTML document is parsed. From the browser’s perspective, there is no LCP candidate in the HTML until the JS executes, which means the LCP clock runs until after JS parsing, execution, and DOM mutation are complete. On a 150ms TTFB page, this might not become a problem. On a page with a 400KB React bundle and a 200ms hydration cycle, it adds 600ms or more to LCP. Server-side rendering or static generation of the hero content is the correct fix — not bundle splitting, not code splitting, but actually producing the LCP element in the initial HTML response.

Oversized or incorrectly formatted images. A JPEG hero at original resolution — 2400×1600px, no compression pass — served to a 390px-wide mobile screen is wasting transfer budget. The image transfer takes far longer than it needs to, expanding the resource load time sub-part. The fixes: serve appropriately-sized images via srcset, use AVIF as the primary format with WebP as fallback (JPEG only for browsers that support neither), and run images through a compression pass targeting visual equivalence rather than exact quality percentages.

Fonts blocking text paint. For pages where the LCP element is a large heading rather than an image, web font loading behavior determines LCP. The default browser behavior (font-display: auto) often blocks text rendering until the custom font downloads, producing FOIT (flash of invisible text). A font-display: swap or font-display: optional setting allows the browser to render with a fallback font and swap when the custom font arrives. optional is the most aggressive — if the font isn’t cached, the browser skips the custom font entirely for this page load — which is often the right call for LCP optimization.

The Right Fixes

Most LCP optimizations reduce to a small set of changes applied consistently:

Add fetchpriority="high" to the LCP image element. This is distinct from preload — it signals resource priority to the browser’s fetch scheduler. Combined with preload, it ensures the image is discovered early and fetched at maximum priority. Without fetchpriority, even preloaded images may be deprioritized if the browser is fetching many resources simultaneously.

Never use loading="lazy" above the fold. Audit every image in the viewport at initial load and ensure none carries the lazy attribute. An automated check for this — which VisibilityIQ runs as part of its render-parity audit — catches cases where a lazy-attributed image becomes the LCP candidate after a layout shift or viewport resize.

Inline critical CSS or preload the critical stylesheet. The goal is to eliminate the render-blocking request chain from document parse to first paint. For most production sites, a critical CSS inlining pipeline is more maintainable than attempting to restructure stylesheet loading manually.

Ensure the LCP element exists in the initial HTML. If your hero is rendered by JavaScript, move its initial render server-side. This applies equally to traditional SSR frameworks and modern edge-rendered Astro/Next.js/Nuxt setups — JavaScript SEO and SSR visibility decisions covers the trade-off in depth.

Use AVIF with WebP fallback. AVIF provides 50–60% smaller files than JPEG at equivalent visual quality. Browser support is broad enough in 2026 that AVIF should be the primary format, with WebP as fallback and JPEG only for legacy browsers. Serve via <picture> with <source type="image/avif"> first.

Set explicit width and height attributes on images. This allows the browser to reserve layout space before the image loads, preventing layout shift that pushes the LCP candidate out of the viewport — but that’s properly a CLS problem. More directly relevant here: sized images don’t require a layout recalculation after download, which reduces element render delay.

Field vs. Lab and Why They Disagree

Lighthouse LCP is measured in a simulated environment: a single device profile, a single viewport, a cable-equivalent network connection. CrUX LCP is the real-world 75th-percentile value across all users visiting the page over the trailing 28 days, segmented by mobile and desktop.

The two will differ whenever: your real user device mix skews slower than Lighthouse’s simulation; your hero image is served at different sizes to different devices and the mobile version takes longer to download; your CDN caches the page for some users and not others; or your page is served from different edge locations with different TTFB characteristics.

The 2.5s threshold for a “good” LCP applies to the CrUX p75 field value — that is the number that appears in Google Search Console’s Core Web Vitals report and is used in the Page Experience ranking signal. A Lighthouse score of 1.8s with a CrUX p75 of 3.2s means you have a real user problem that your lab tooling isn’t surfacing. VisibilityIQ reconciles CrUX field data against lab measurements and flags the gap — particularly cases where field LCP is in the “needs improvement” or “poor” bands even though Lighthouse produces a passing score.

The INP metric that replaced FID follows the same field-vs-lab dynamic, and the diagnostic approach is similar: profile the specific interaction, attribute it to a sub-part, fix the dominant sub-part. The sub-part framing applies to all three CWV signals and is the most efficient way to avoid treating symptoms rather than causes.

For pages where the LCP element may shift based on render-parity differences between raw HTML and the client-rendered DOM — common with SPAs where hydration changes the largest element — the LCP value is non-deterministic until rendering behavior is locked down. Stabilizing what element is the LCP candidate is a prerequisite to optimizing how fast it loads.

The Practical Sequence

Start with sub-part attribution in the Performance panel. Identify which phase — TTFB, resource load delay, resource load time, or element render delay — accounts for the majority of your LCP time. Fix that phase first.

If resource load delay is the problem, add preload and fetchpriority="high", and audit for lazy attributes on the LCP candidate. If resource load time is the problem, convert the image to AVIF and serve it at the correct dimensions. If element render delay is the problem, profile main-thread blocking post-download and eliminate long tasks. If TTFB is the problem, optimize your server response or CDN configuration — but verify that TTFB is actually the dominant sub-part before spending time there.

Run the fix against your lab tool, then wait 28 days for CrUX to reflect the change in field data. The gap between lab verification and field verification is unavoidable — it’s not a measurement failure, it’s the nature of measuring real-user performance at population scale.

LCP optimization is mostly a discovery and sequencing problem. The image is usually fine once the browser finds it early and fetches it at high priority. The work is in the request chain that gets the browser to that point as fast as possible.

Frequently asked questions

What counts as the LCP element and does it change between lab and field?
The LCP element is the largest visible element in the initial viewport at load time — almost always a hero image, a large heading, a background image, or an above-fold video poster frame. In lab tools like Lighthouse, the element is identified at a fixed simulated viewport. In field data (CrUX), it's measured across the actual device and viewport distribution of real users, which often means the element differs on mobile versus desktop. This discrepancy is one reason field LCP and lab LCP diverge.
How much does fetchpriority actually help for LCP images?
Significantly, when applied correctly. Without fetchpriority, browsers assign images a 'low' or 'medium' network priority until the layout engine determines they're in the viewport, which happens late in the parse/layout pipeline. Adding fetchpriority='high' to the LCP image moves it into the browser's high-priority fetch queue immediately, eliminating most of the resource load delay sub-part. Google's own testing showed median LCP improvements of 20–30ms on well-optimized pages and hundreds of milliseconds on pages where the image was previously deprioritized.
If my server TTFB is fast, why is my LCP still slow?
Fast TTFB eliminates only one of LCP's four sub-parts. You can have a 100ms TTFB and still have a slow LCP if the browser doesn't discover the LCP image until late (resource load delay), if the image file is large and takes time to transfer (resource load time), or if render-blocking CSS or JavaScript is holding up paint (element render delay). LCP is a loading-sequence problem as much as a server-speed one — each sub-part needs to be profiled separately.