All WordPress HTML Templates Forms & Webhooks AI & Tools
HTML Templates

Page Speed for Static Sites: The Wins That Actually Move LCP

Improve LCP on static sites with the fixes that actually work: fetchpriority, preload scanner discoverability, font render delay, CDN cache and field data.

A real working moment illustrating the theme of an article about improve lcp. Wide 16:9 banner, one strong focal point, magazine editorial quality, authentic and unstaged.

A client came to us last year with a Lighthouse score of 98 on desktop, 94 on mobile, and a Search Console report saying 71% of their URLs were failing Core Web Vitals. The site was a statically generated marketing site on a decent CDN. Nothing was obviously broken. The hero image was 34KB of AVIF, the CSS was 11KB gzipped, and there were exactly two scripts on the page.

Their real p75 LCP was 3.9 seconds. The fix took about forty minutes and involved deleting a line of JavaScript and adding one HTML attribute. That gap between a lab score and what real phones on real networks experience is where most page speed work goes wrong. You can improve LCP enormously on a static site, but only if you stop treating it as a score and start treating it as four measurable phases you can attack independently.

Key Takeaways

  • Break every LCP investigation into TTFB, resource load delay, resource load time and element render delay. On static sites the first and third are usually fine. The damage concentrates in load delay and render delay.
  • The LCP element must be a plain <img> in the initial HTML response with fetchpriority="high". CSS backgrounds, JS-injected images, carousels and client-rendered heroes all push discovery past the preload scanner and cost 300ms to 1,200ms on mobile.
  • Render delay is the silent killer: blocking fonts, a theme-flash prevention script, and a hydration pass can keep a fully downloaded image invisible for half a second.
  • AVIF is not automatically the right format. On low-end Android, decode time for a large AVIF can exceed the bytes saved, so compare encoded size against decode cost for your actual hero dimensions.
  • Lighthouse is a debugger, not a scoreboard. Ship the web-vitals attribution build and make decisions on p75 field data from real devices.

Break LCP into its four phases before touching anything

LCP is not one number. Chrome decomposes it into time to first byte, resource load delay (the gap between TTFB and when the browser actually starts the LCP resource request), resource load time (the download itself), and element render delay (download finished, pixels not yet painted). Every optimisation you can make targets exactly one of these.

Get the breakdown first. Open the Performance panel in DevTools, record a load with CPU throttled to 4x and network set to Slow 4G, and click the LCP marker in the Timings track. Chrome prints the four phases with millisecond values. Do this before you open your bundler config.

The pattern on static sites is consistent. TTFB from a warm CDN edge lands between 40ms and 180ms. Download of a well-sized hero on Slow 4G is maybe 200ms to 400ms. Then there’s 900ms of load delay and 400ms of render delay that nobody budgeted for. Static hosting already solved the part everyone talks about. The remaining time is almost entirely self-inflicted by the front end.

A real working moment that fits an article section about

Make the LCP element discoverable in the raw HTML

Chrome’s preload scanner reads the HTML byte stream and starts fetching images before the parser has finished, before CSS has been applied, before any JavaScript has run. Anything that hides the hero image from that scanner adds a full round trip plus parse time to your LCP.

Four things reliably hide it:

  • CSS background images. The browser cannot know the URL until the CSSOM is built and the element is matched to a rule. Easily 200ms to 500ms of load delay.
  • loading="lazy" on the hero. Still happens, usually because a CMS or component library applies it globally. Lazy images are deprioritised and deferred until layout, which is categorically wrong for anything above the fold.
  • Carousels. A slider that builds its slides in JS means the first slide image doesn’t exist in the DOM when the scanner runs. We’ve measured 600ms+ of pure load delay from this alone, which is one more reason to read the case for not using a carousel before you put one in a hero.
  • Client-side rendered heroes. If your “static” site ships an empty root div and hydrates, your LCP floor is your JS execution time. No image attribute saves you.

The target markup is boring, and that’s the point:

<img
  src="/img/hero-1200.avif"
  srcset="/img/hero-640.avif 640w, /img/hero-1200.avif 1200w, /img/hero-1920.avif 1920w"
  sizes="(min-width: 1024px) 60vw, 100vw"
  width="1200" height="800"
  alt="Dashboard showing weekly revenue"
  fetchpriority="high"
  decoding="async">

fetchpriority="high" is the single highest-leverage attribute on the page. Without it, Chrome initially assigns images a Low priority until layout proves they’re in the viewport, then upgrades them. That upgrade costs real time on a congested connection. Support is universal now: Chrome since 101, Safari 17.2, Firefox 132. Use it on exactly one element per page. Marking three images as high priority is the same as marking none.

Get sizes right or the rest is theatre. A sizes="100vw" on an image that renders at 600px wide inside a container makes a 1440px-wide laptop download the 1920w source. We’ve seen that mistake triple resource load time on sites that were otherwise immaculate. If your hero is constrained by a grid, express that constraint honestly. If you cannot reason about it, measure the rendered width in DevTools and work backwards.

When the LCP element is text

Plenty of well-built pages have a heading as the LCP element, which is usually a sign the hero section is doing its job. In that case load delay is irrelevant and your entire LCP is TTFB plus render delay, which means fonts and render-blocking CSS are your only levers.

Render delay is where static site performance quietly leaks

The image arrived at 800ms. LCP fired at 1,340ms. Where did 540ms go?

Fonts, usually. A @font-face with the default font-display: auto gives Chrome a block period of roughly three seconds during which the text is invisible. If your LCP candidate is text, that’s your LCP. font-display: swap fixes the invisibility but introduces layout shift, and the shift is what tanks CLS. The fix that actually works is a metrics-matched fallback:

@font-face {
  font-family: "Inter Fallback";
  src: local("Arial");
  / Tuned so Arial occupies the same box as Inter: no reflow on swap /
  size-adjust: 107%;
  ascent-override: 90%;
  descent-override: 22.4%;
  line-gap-override: 0%;
}

body { font-family: "Inter", "Inter Fallback", sans-serif; }

Generate those numbers with Capsize or the Fontaine plugin rather than guessing. Pair it with <link rel="preload" as="font" type="font/woff2" crossorigin> for the one weight used above the fold, and nothing else. Preloading six weights creates a self-inflicted bandwidth contention problem that directly delays your hero image.

The second render delay source is the inline script everyone adds to stop the flash of wrong theme. It’s correct to have one, it’s correct for it to be synchronous in the head, and it is also parser-blocking. Keep it under about 500 bytes: read from localStorage, set one attribute on <html>, stop. We go through the whole pattern in dark mode done properly. If that script is 4KB because someone imported a helper, you’re paying for it on every single page load.

Third: anything that defers painting behind JavaScript. Scroll-reveal animations that start elements at opacity: 0 and fade them in on an IntersectionObserver callback will make your hero’s LCP equal to your JS execution time. If you must animate the hero, animate transform only, start from an opaque state, or trigger it from CSS. This is the progressive enhancement argument reduced to a metric: content first, enhancement second.

Edge configuration that genuinely moves the needle

Static hosting gives you a good TTFB only on a cache hit. Check your CDN’s hit ratio before assuming. A site with per-country redirect rules, a cookie-varying header, or query strings appended by ad platforms can have a cache hit rate under 60% without anyone noticing. That means a meaningful share of users are waiting on your origin.

Three settings worth getting right:

# Hashed assets: never revalidate
/assets/*
  Cache-Control: public, max-age=31536000, immutable

HTML: serve instantly from edge, revalidate in background

/*.html Cache-Control: public, max-age=0, s-maxage=86400, stale-while-revalidate=604800

stale-while-revalidate on HTML is the underused one. It turns a deploy-triggered cache miss from a user-visible origin fetch into a background refresh. On compression: enable Zstandard if your CDN offers it (Chrome has supported Content-Encoding: zstd since version 123), but the delta over Brotli at level 11 for a 12KB HTML document is a couple of milliseconds. Don’t spend an afternoon on it.

Speculation Rules are worth more. On a multi-page static site, prerendering the likely next document makes navigation LCP effectively zero:

<script type="speculationrules">
{
  "prerender": [{
    "where": { "href_matches": "/*" },
    "eagerness": "moderate"  / fires on hover or pointerdown, not on sight /
  }]
}
</script>

One real caveat: prerendering executes the page, including analytics. Chrome defers most of it until activation, but check that your tag manager handles the prerender state or you’ll inflate pageviews. Keep eagerness at moderate on content-heavy sites. eager on a 40-link nav will burn user bandwidth for nothing.

Measure in the field, debug in the lab

Lighthouse runs a simulated Moto G Power on a modelled network. Your users are on a four-year-old Android with 14 tabs open and battery saver throttling the CPU. Those are different planets. Ship this instead:

import { onLCP } from 'web-vitals/attribution';

onLCP(({ value, attribution }) => {
  // The attribution build gives you the phase breakdown per real user
  navigator.sendBeacon('/collect', JSON.stringify({
    value,
    element: attribution.target,            // CSS selector of the LCP element
    ttfb: attribution.timeToFirstByte,
    loadDelay: attribution.resourceLoadDelay,
    loadTime: attribution.resourceLoadDuration,
    renderDelay: attribution.elementRenderDelay,
    url: location.pathname
  }));
}, { reportAllChanges: false });

Within a week you’ll know which template has the problem and which phase owns it. That’s a different conversation from “our score dropped to 87”. Segment by device class and connection type before drawing conclusions. An aggregate p75 across desktop and mobile hides everything interesting.

Things that are not worth your time

  • Automated critical CSS extraction. For a static site whose total CSS is under about 20KB compressed, inlining the whole stylesheet is simpler and often faster than running a critical-path tool that breaks on every component change. Above roughly 50KB it starts paying off. The middle ground is maintenance debt.
  • Converting everything to AVIF. AVIF typically beats WebP on size by 20% to 30%, but decoding a 1920px AVIF on a low-end Snapdragon can take 80ms or more against 25ms for a JPEG. For a hero at 1200px wide, test both. Below-fold images, use AVIF freely.
  • Preloading more than two resources. Preloads compete with each other and with the LCP image. Every addition dilutes the signal.
  • Chasing 100 in Lighthouse. The gap between 92 and 100 is almost always third-party scripts you cannot remove and unused CSS warnings that do not reflect runtime cost.
  • Micro-optimising TTFB on a CDN that already returns in 90ms. Spend that hour on render delay.

One honest limit on all of this: if the page carries a chat widget, a consent banner and a tag manager, your hero work can be undone by a third party that injects a full-screen overlay at 1.8 seconds. Measure what the third parties cost by blocking them in DevTools, then take the number to whoever owns that decision. Our starting point on new builds is a template with the hero markup already correct: the Canvas demos ship the eager, prioritised <img> pattern rather than CSS backgrounds, which removes the most common 500ms mistake before anyone writes a line of code.

Frequently Asked Questions

What is a good LCP target in 2026?

Under 2.5 seconds at the 75th percentile of real users, measured separately for mobile and desktop. Anything over 4.0 seconds is classified as poor. We aim for 2.0 seconds on mobile as an internal target, because that leaves headroom for the third-party scripts a client will inevitably add after launch.

Does fetchpriority=”high” work if the image is below the fold?

It works, in the sense that the browser will fetch it with high priority, which is exactly what you don’t want. Use it only on the element that is actually the LCP candidate, confirmed in the Performance panel. Applying it to multiple images flattens priorities and makes the real LCP element arrive later.

Should I preload the hero image as well as setting fetchpriority?

Usually not. If the image is a plain <img> near the top of the HTML, the preload scanner finds it just as fast and the preload tag is redundant weight. Preload is for images the scanner cannot see, such as a CSS background or an art-directed <picture> source you must reference through a Link header.

Why is my LCP worse after moving to a static site generator?

Almost always because the generator ships a client-side hydration bundle that re-renders the hero, or because the image component applies loading="lazy" by default. Check whether your framework’s image helper lets you set priority on the hero, and verify in view-source that the image URL is present in the HTML response, not just in the hydrated DOM.

Do Core Web Vitals still affect rankings?

They remain a ranking signal, but a weak one compared to content relevance. The stronger argument is commercial: slow heroes correlate with bounce on mobile, and the fixes described here take hours, not sprints. Treat page speed optimization as a conversion exercise and the SEO benefit as a side effect.

Here’s the decision to make this week: record one Performance trace of your highest-traffic template on Slow 4G with 4x CPU throttling, read the four LCP phases, and fix only the largest one. If it’s load delay, your hero isn’t discoverable. If it’s render delay, it’s fonts or a blocking script. If it’s TTFB, check your CDN hit ratio. Three possibilities, three known fixes, and you’ll know which one you have in under ten minutes.

element render delay font-display fetchpriority high lcp image improve lcp improve lcp static site lcp resource load delay fix speculation rules prerender static site stale-while-revalidate html caching cdn web-vitals attribution build lcp