Fixing LCP When the Hero Is a Background Image

Many static site themes build the hero section with a CSS background image: a full-width section with background-image: url(hero.jpg), background-size: cover and a heading on top. It is a convenient way to put text over a photo, and it quietly costs hundreds of milliseconds of LCP. The browser's preload scanner, which finds images in the HTML while the page is still downloading, cannot see images referenced in CSS. The request starts only after the stylesheet has downloaded, been parsed and matched to the element — and even then, CSS gives the browser no way to pick a size or raise the image's priority.

This guide measures the cost on a marketing site built with an Eleventy theme whose landing pages use background heroes, then fixes it two ways — replacing the background with an <img> and, where the markup cannot change, preloading the image — and compares the results. It is part of Largest Contentful Paint Optimization for Static Sites.

Prerequisites

  • A page whose LCP element is a background image (check with the Performance panel's LCP marker or the web-vitals attribution element field).
  • Access to the template that renders the hero and to the image pipeline.
  • Lighthouse and field LCP data to compare before and after.

Step 1: See the Delay

The waterfall for a landing page on a throttled mobile profile showed the problem clearly:

EventTime
HTML first byte180 ms
Stylesheet request starts230 ms
Stylesheet finished610 ms
Hero image request starts690 ms
Hero image finished (1920 px JPEG, 312 KB)1,820 ms
LCP1,900 ms (lab), 2.8 s field p75

The hero request waited 460 ms after the HTML started arriving — for the CSS — and then downloaded a 1920-pixel JPEG on a 390-pixel phone, because CSS backgrounds have no srcset. In field data the LCP element for this template was the hero section in 94% of mobile page views.

Waterfall timeline for a CSS background hero on mobile A timeline of one throttled mobile load. The HTML first byte arrives at 180 milliseconds. The stylesheet request starts at 230 milliseconds and finishes at 610 milliseconds. Only then, at 690 milliseconds, does the hero image request start, and the 312 kilobyte JPEG finishes at 1,820 milliseconds. LCP is at 1,900 milliseconds. Why the background hero loads late HTML first byte CSS loaded Image request starts LCP 180 ms 610 ms rule matched to hero 690 ms 1920 px JPEG 1,900 ms after 312 KB download
The image request waits for the stylesheet because the preload scanner cannot see url() references in CSS.

Fix 1: Replace the Background With an img

The most effective fix is to make the image part of the HTML. An <img> absolutely positioned behind the hero content, with object-fit: cover, looks identical to background-size: cover:

<section class="hero">
  <img class="hero-bg" src="/img/hero-1280.avif"
       srcset="/img/hero-640.avif 640w, /img/hero-960.avif 960w, /img/hero-1280.avif 1280w, /img/hero-1920.avif 1920w"
       sizes="100vw" width="1920" height="900" alt="" fetchpriority="high" decoding="async">
  <div class="hero-content">
    <h1>Ship documentation your users can find</h1>
    <p>…</p>
  </div>
</section>
.hero { position: relative; isolation: isolate; min-height: 60vh; display: grid; align-items: end; }
.hero-bg { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: 60% 40%; z-index: -1; }

Now the preload scanner finds the image as soon as the HTML arrives, srcset and sizes let a phone download the 640-pixel AVIF (41 KB) instead of the 1920-pixel JPEG (312 KB), and fetchpriority="high" puts it ahead of other images. alt="" marks it decorative, which matches how the background was treated; if the image carries meaning, give it real alt text. object-position replaces background-position.

The Eleventy theme rendered the hero from frontmatter (hero: /img/team.jpg); changing the hero partial to use the image shortcode was a one-file change that fixed every landing page.

Fix 2: Preload When Markup Cannot Change

Some heroes cannot become an <img> — a theme you do not control, a CMS that injects inline style="background-image:…", or a layered effect built on multiple backgrounds. Then preload the image in the <head> so the request starts with the HTML:

<link rel="preload" as="image" fetchpriority="high"
      imagesrcset="/img/hero-640.avif 640w, /img/hero-960.avif 960w, /img/hero-1280.avif 1280w, /img/hero-1920.avif 1920w"
      imagesizes="100vw" type="image/avif">

and use image-set() in the CSS so the background can at least pick a format and resolution:

.hero { background-image: image-set(url('/img/hero-1280.avif') type('image/avif'), url('/img/hero-1280.jpg') type('image/jpeg')); }

A caveat: the preload chooses a width from imagesrcset and imagesizes, while the CSS picks its own URL. Unless the CSS references the same file the preload selected, the browser downloads two images. On this site the CSS used a single 1280-pixel AVIF, so the preload was narrowed to that one file (href instead of imagesrcset), accepting a larger-than-necessary file on phones in exchange for no double download. That is why Fix 1 is better.

Comparison of background, preloaded background and img hero A matrix comparing three approaches. The plain CSS background is discovered after CSS, cannot choose a size and cannot raise priority. A preloaded background is discovered with the HTML and can raise priority but has limited size selection and a risk of double download. An img with srcset and fetchpriority is discovered with the HTML, chooses the right size, raises priority and has no double-download risk. Three ways to ship the hero Approach Discovery Right size Priority CSS background after CSS one URL default Preload + background with HTML one URL safely high img + object-fit with HTML srcset + sizes fetchpriority
The img approach wins on every column; preloading is the fallback when markup is fixed.

Step 2: Check Overlays and Text Contrast

Background heroes often rely on a gradient overlay for text contrast, written as a second background layer. With an <img>, put the overlay on a pseudo-element of the section instead:

.hero::before { content: ''; position: absolute; inset: 0; z-index: -1;
  background: linear-gradient(to top, rgb(0 0 0 / .65), rgb(0 0 0 / 0) 60%); }

The pseudo-element paints above the image (both are behind the content thanks to isolation: isolate and z-index: -1 in document order), and the heading keeps its contrast. Verify with axe in CI that the text still passes contrast against the darkest and lightest parts of the image; automated checks cannot always compute contrast over images, so also check visually at several viewport widths.

Finding Background Heroes Across a Site

On a large site, background heroes hide in more places than the landing page template: section index pages, author pages, campaign pages built by marketing, and blog posts with a "cover image" rendered as a background. A quick audit finds them. Search the built CSS and HTML for background-image and inline style attributes with url(, then cross-reference with field LCP attribution, which records the LCP element's selector. On this site the search found five templates using background heroes; field data showed three of them were the LCP element for most page views, and two were below the fold and harmless. Fixing the three and leaving the other two alone saved effort without leaving LCP on the table.

A CSS lint rule can stop new ones: a Stylelint declaration-property-value-disallowed-list entry that flags background-image with url() in files under the hero and banner component directories, with a comment pointing to the <img> pattern. Decorative backgrounds elsewhere, such as subtle textures, are allowed; they are small and never the LCP element.

Keep one special case in mind: some themes use a tiny blurred background image as a placeholder behind a hero <img>. That is fine — it is inline or very small — and it complements the approach in Blur-Up Placeholders with LQIP.

Measured Impact

Five landing pages, Lighthouse mobile median of five runs, and two weeks of field data:

MeasureCSS backgroundPreload + backgroundimg + srcset + fetchpriority
Hero request start (lab)690 ms210 ms205 ms
Hero bytes on a phone312 KB118 KB (single AVIF)41 KB
LCP (lab)1.90 s1.24 s0.98 s
LCP p75, mobile field2.8 s— (not shipped)1.7 s
CLS p750.020.020.01
LCP and hero image size for background, preloaded background and img Paired bars comparing the plain CSS background with the img approach. Lab LCP fell from 1.90 seconds to 0.98 seconds. Hero bytes on a phone fell from 312 kilobytes to 41 kilobytes. Lab LCP and hero bytes by approach CSS background img + srcset + fetchpriority Hero bytes on a phone 312 KB 41 KB Lab LCP (x100 ms scale) 1.90 s 0.98 s
Lighthouse mobile, median of five runs across five landing pages; the preload variant sat between the two.

Pitfalls & Rollback

  • Preload and CSS picking different files. The browser downloads both; make them match or use a single URL.
  • Lazy-loading the hero <img>. The LCP image must be eager; loading="lazy" on it delays LCP further.
  • Losing the overlay. Move gradient overlays to a pseudo-element, and recheck contrast.
  • Meaningful images with empty alt. If the hero conveys information, describe it.
  • Rollback: the hero partial is one template; restoring the background version is a single revert.

Conclusion

A hero painted with a CSS background image is invisible to the preload scanner, cannot be sized per device and cannot be prioritised — three problems for the element that usually decides LCP. Replace it with an <img> using object-fit: cover, srcset, sizes and fetchpriority="high"; preload only when the markup cannot change. On this site the change halved lab LCP and took field LCP from 2.8 s to 1.7 s with no visual difference.

FAQ

Why are CSS background images bad for LCP?

The browser only discovers a background image after it has downloaded and parsed the stylesheet and matched the rule to an element, so the request starts late. It also cannot use srcset, sizes, loading or fetchpriority attributes, so it cannot choose a size or raise priority.

Do CSS background images count as LCP elements?

Yes. Elements with a background image loaded via url() are LCP candidates, so a large hero background is often the LCP element even though it is invisible to the preload scanner.

What is the best fix?

Replace the background with a real img element positioned behind the content with object-fit cover. That makes the image discoverable in the HTML and gives it srcset, sizes and fetchpriority. When that is not possible, preload the image with imagesrcset and fetchpriority high.

Does image-set in CSS solve the problem?

It adds resolution and format selection to CSS backgrounds, which helps bytes, but it does not fix late discovery or priority. It is useful for decorative backgrounds, not for the LCP image.

Filed underFixes