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-vitalsattributionelementfield). - 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:
| Event | Time |
|---|---|
| HTML first byte | 180 ms |
| Stylesheet request starts | 230 ms |
| Stylesheet finished | 610 ms |
| Hero image request starts | 690 ms |
| Hero image finished (1920 px JPEG, 312 KB) | 1,820 ms |
| LCP | 1,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.
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.
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:
| Measure | CSS background | Preload + background | img + srcset + fetchpriority |
|---|---|---|---|
| Hero request start (lab) | 690 ms | 210 ms | 205 ms |
| Hero bytes on a phone | 312 KB | 118 KB (single AVIF) | 41 KB |
| LCP (lab) | 1.90 s | 1.24 s | 0.98 s |
| LCP p75, mobile field | 2.8 s | — (not shipped) | 1.7 s |
| CLS p75 | 0.02 | 0.02 | 0.01 |
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.
Related
- Parent: Largest Contentful Paint Optimization for Static Sites — all LCP subparts.
- Reducing LCP from Hero Images on Static Sites — sizing and formats for hero images.
- Optimizing LCP on Astro with Priority Hints — fetchpriority in practice.
- Art Direction with the Picture Element — different crops for the same hero.
- Fetchpriority Beyond the Hero Image — priority for everything else.
Filed underFixes