Largest Contentful Paint Optimization for Static Sites

Largest Contentful Paint (LCP) measures how long it takes for the largest visible element in the viewport — usually a hero image, a heading, or a background image — to render. Static site generators hand you a head start because the HTML arrives pre-rendered in a single response, but the LCP element still has to be discovered, downloaded, and painted. This guide is the complete LCP workflow for static sites: find the LCP element, get it requested as early as possible, serve it in a fast format, and stop CSS and fonts from blocking the paint. It sits within the broader Performance Optimization & Core Web Vitals for SSGs effort, where LCP is the metric most directly tied to perceived load speed.

Every step below ties to a measured number and the tool that produced it (Lighthouse, lhci, WebPageTest). The running example is a documentation landing page built with Astro, deployed to a CDN, and measured on a throttled mid-tier mobile profile (Moto G-class, 4x CPU slowdown, ~1.6 Mbps). The baseline LCP was 3.4s; the finished page lands at 1.6s.

Where the LCP element's time goes A waterfall showing the four spans that make up Largest Contentful Paint on a static site: time to first byte, resource discovery delay, image download, and render delay, with the levers that shrink each span. LCP = TTFB + discovery + download + render delay TTFB edge cache Discovery preload & priority Download AVIF/WebP size Render delay critical CSS & fonts Same spans as a waterfall — shrink the widest first 0.2s discovery 0.9s download 1.4s render 0.9s Baseline waterfall — total LCP 3.4s. The download and discovery spans are the widest, so they get fixed first. After preload + AVIF + critical CSS, every span shrinks and total LCP falls to 1.6s.
LCP is the sum of four spans. Measure the waterfall, find the widest span, and apply the matching lever — discovery, download size, or render delay.

Step 1: Identify the LCP Element

You cannot optimize what you have not measured. Before touching markup, find the exact element the browser counts as the largest contentful paint, because the fix is different for an image, a heading, or a CSS background.

Lighthouse reports it directly. Run it against a deployed URL and open the Largest Contentful Paint element audit, which names the DOM node and the four sub-parts of LCP (TTFB, load delay, load time, render delay). For a programmatic check, drop a PerformanceObserver into the console:

new PerformanceObserver((list) => {
  const entry = list.getEntries().at(-1);
  console.log('LCP element:', entry.element, 'at', Math.round(entry.startTime), 'ms');
}).observe({ type: 'largest-contentful-paint', buffered: true });

On our example page the LCP element was the hero <img>, paint timestamp 3,400 ms. WebPageTest confirmed it by marking the hero frame in the filmstrip. Knowing the element is an image — not text — told us the dominant cost was the download span, so that is where we started.

LCP element typeMost likely costPrimary lever
Hero <img>Download + late discoveryPreload, fetchpriority, AVIF/WebP
CSS background imageVery late discoveryPreload (the parser can't see it in markup)
Heading / text blockRender delay from CSS + fontsInline critical CSS, preload font

Step 2: Make the LCP Resource Discoverable Early

The single biggest waste in LCP is a late request. The browser's preload scanner finds resources by reading HTML, so an <img> near the top is found quickly — but a CSS background-image, an image injected by a script, or an image far down the DOM is discovered late, after the stylesheet that referenced it has already downloaded.

Two tools fix discovery. First, fetchpriority="high" tells the browser this image matters more than the other images it found, so it gets a connection ahead of them:

<img src="/hero.avif" alt="Product dashboard" width="1200" height="600"
     fetchpriority="high" />

Second, for resources the parser cannot see early — background images, or an image the framework outputs late — add an explicit preload in <head>:

<link rel="preload" as="image" href="/hero.avif" fetchpriority="high" />

In our measurement, adding fetchpriority="high" plus a preload moved the hero request from 910 ms to 120 ms after navigation start and cut LCP from 3.4s to 2.7s — a 0.7s win with two lines of HTML. The Astro-specific mechanics (the priority prop, eager loading, and where preload belongs) are covered in Optimizing LCP on Astro with Priority Hints.

Step 3: Shrink the LCP Image Download

Discovery only helps if the file is small enough to arrive quickly. The download span is usually the widest part of LCP on an image-led page, and the fix is the same build-time work that drives the whole Image Optimization Pipelines in Astro approach: serve a modern format at the right size.

  • Format: AVIF is typically 20-30% smaller than WebP at matched quality, and WebP is 25-50% smaller than JPEG. Emit both with a fallback.
  • Dimensions: never ship a 2400px source into a 1200px slot. Generate the widths the layout actually uses and let srcset pick.
  • Never lazy-load the hero: loading="lazy" on the LCP image defers it behind layout, which is the opposite of what you want.
<picture>
  <source type="image/avif" srcset="/hero-800.avif 800w, /hero-1200.avif 1200w"
          sizes="(max-width: 800px) 100vw, 1200px" />
  <source type="image/webp" srcset="/hero-800.webp 800w, /hero-1200.webp 1200w"
          sizes="(max-width: 800px) 100vw, 1200px" />
  <img src="/hero-1200.jpg" alt="Product dashboard" width="1200" height="600"
       fetchpriority="high" decoding="async" />
</picture>
Hero source (1200px)BytesLCP (throttled mobile)
Original JPEG q901.4 MB2.7s
WebP q80240 KB1.9s
AVIF q80180 KB1.7s

Swapping the 1.4 MB JPEG for a 180 KB AVIF cut the download span from 1.4s to 0.3s and brought LCP to 1.7s. The full hero recipe — sizing, srcset, and the lazy-loading trap — is in Reducing LCP from Hero Images on Static Sites.

Step 4: Remove Render-Blocking CSS

Even a tiny LCP element cannot paint until the CSS that styles the page has loaded and parsed. A single external stylesheet in <head> blocks the first paint for an entire network round trip. The fix is to inline the small slice of CSS needed to render the above-the-fold view and load the rest non-blocking:

<head>
  <style>/* critical CSS: layout, hero, typography for the first viewport */</style>
  <link rel="preload" href="/styles.css" as="style"
        onload="this.onload=null;this.rel='stylesheet'" />
  <noscript><link rel="stylesheet" href="/styles.css" /></noscript>
</head>

On our page the external stylesheet was 78 KB and blocked first paint for ~0.6s. Inlining a 9 KB critical slice and deferring the rest moved First Contentful Paint earlier and shaved the LCP render delay, contributing the final step from 1.7s to 1.6s while improving FCP more dramatically (2.1s to 0.9s). The full critical-CSS and purge workflow — including how to generate the critical slice and prune unused rules — is in Eliminating Render-Blocking CSS on Static Sites.

Step 5: Keep Fonts From Delaying the Paint

When the LCP element is a text block, web fonts become the bottleneck. With font-display: swap the browser paints fallback text first and repaints when the web font arrives; if the LCP text uses the web font, its measured paint can move to that later repaint. Two disciplines keep text LCP early:

<link rel="preload" as="font" type="font/woff2"
      href="/fonts/inter-var.woff2" crossorigin />
@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter-var.woff2') format('woff2');
  font-display: swap;
  size-adjust: 102%; /* metric-matched fallback to avoid reflow */
}

Preload the one weight that appears above the fold, self-host it to avoid a third-party connection, and use a metric-matched fallback so the swap does not shift layout. The full font workflow lives in Font Loading Strategies for Static Sites. On text-led pages, preloading the hero weight typically moves LCP earlier by 200-400 ms because the heading paints in its final font on the first paint.

Common Pitfalls

  • Preloading the wrong resource: a preload for an image the page does not actually use as its LCP wastes bandwidth and can delay the real LCP resource. Always re-run Lighthouse after adding a preload.
  • fetchpriority="high" on many images: if everything is high priority, nothing is. Reserve it for the single LCP element.
  • Lazy-loading the hero: loading="lazy" on the above-the-fold image is the most common self-inflicted LCP regression on static sites.
  • Critical CSS drift: generated critical CSS goes stale when the design changes. Regenerate it in the build, not by hand, or it will block paint with rules the page no longer needs.
  • Counting lab numbers as field numbers: a great Lighthouse LCP can still be poor in the field if real users are on slower networks. Confirm with RUM at the 75th percentile.

Key Takeaways

  • LCP is four spans — TTFB, discovery, download, render delay. Measure the waterfall and fix the widest first.
  • Identify the LCP element before optimizing; the fix differs for an image, a background, or text.
  • Make the LCP resource discoverable early with fetchpriority="high" and a targeted preload.
  • Shrink the image with AVIF/WebP at the right width, and never lazy-load the hero.
  • Inline critical CSS and preload the above-the-fold font so render delay does not gate the paint.

FAQ

What is a good LCP target for a static site?

Aim for 2.5 seconds or less at the 75th percentile of real users, which is the "good" threshold Google uses. On a pre-rendered static page with an optimized hero you can usually reach 1.5 to 2.0 seconds in the lab on a throttled mobile profile, which leaves headroom for slow real-world networks.

How do I find which element is the LCP element?

Run Lighthouse and open the "Largest Contentful Paint element" audit, or use the PerformanceObserver API for largest-contentful-paint entries in the browser console. WebPageTest also marks the LCP frame in its filmstrip. The element is most often a hero image, a heading, or a background image.

Does preloading the LCP image always help?

It helps when the image is discovered late — for example a CSS background image or an image deep in the DOM. If the image is already an early <img> tag with fetchpriority="high", an extra preload adds little and can waste bandwidth on the wrong resource, so always measure before and after.

Why does CSS affect LCP if my content is just text?

Render-blocking CSS delays first paint entirely. The browser will not paint the LCP text block until the stylesheet that governs it has loaded and parsed. Inlining the critical CSS lets the first paint happen on the initial HTML response instead of waiting for a separate round trip.

Can web fonts make LCP worse?

Yes, when the LCP element is a text block. With font-display: swap the browser may delay painting that text or repaint it when the web font arrives, and the LCP timestamp moves to the later paint. Preloading the one above-the-fold weight and using a metric-matched fallback keeps the text paint early and stable.

Is LCP a problem on static sites at all, given they pre-render HTML?

Pre-rendering gives you a strong starting point because the markup arrives in one response, but the LCP resource itself — a hero image, a font, or a stylesheet — still has to download and render. Those are exactly the things this guide controls, which is why measured static sites still move from roughly 3 seconds to under 2 seconds after this work.

Static Site Generators in Production