Skip to main content

Don't use CSS background-image for LCP content

Use <img> tags for hero and LCP images instead of CSS background images to enable early discovery by the browser's preload scanner.


CSS background images are discovered late in the rendering process, creating a waterfall delay:

  1. HTML downloads and parses.
  2. The CSS file is discovered and downloads.
  3. The CSS parses.
  4. The background image URL is discovered.
  5. The image finally starts downloading.

Using an <img> tag lets the browser's preload scanner discover images during the initial HTML parse, starting the image download immediately in parallel with CSS.

The impact is especially significant when the background image is in an external CSS file, because CSS download latency adds to the discovery delay.

  • You can use fetchpriority="high" to prioritize the LCP image.
  • You can use <link rel="preload"> if needed.
  • Responsive image support with srcset and sizes is better.
  • It's easier to add width and height for CLS prevention.
  • It works with Shopify's image_tag filter and automatic optimization.

Anchor to When background images are acceptableWhen background images are acceptable

  • Decorative patterns or textures.
  • Non-LCP content elements.
  • Elements outside the initial viewport.
  • Small UI elements where discovery latency doesn't matter.
  • Data URIs or inline SVGs, because there's no network request.

Use an <img> tag with absolute positioning to achieve the same visual effect as a background image. Load the image conditionally based on section position using section.index.

Write the condition as a positive section.index > 2 test rather than section.index <= 2. section.index is nil in the online store editor, in static sections, and in the Section Rendering API, and comparisons against nil are falsey, which would send the hero to the lazy branch. For the full explanation, refer to Load above-the-fold and below-the-fold sections differently with section.index.

HTML structure:

<section class="hero">
{% unless section.index > 2 %}
{{ section.settings.image
| image_url: width: 2000
| image_tag:
loading: 'eager',
fetchpriority: 'high',
widths: '600, 900, 1200, 1600, 2000',
sizes: '100vw',
class: 'hero__image'
}}
{% else %}
{{ section.settings.image
| image_url: width: 2000
| image_tag:
loading: 'lazy',
widths: '600, 900, 1200, 1600, 2000',
sizes: '100vw',
class: 'hero__image'
}}
{% endunless %}
<div class="hero__content">
<h1>{{ section.settings.title }}</h1>
</div>
</section>

CSS for positioning:

.hero {
position: relative;
min-height: 400px;
}

.hero__image {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
}

.hero__content {
position: relative;
z-index: 1;
}

The image needs no z-index, because .hero__content already stacks above it. Don't add a negative one: position: relative alone doesn't make .hero a stacking context, so z-index: -1 escapes it and can paint behind a section or wrapper background, leaving the LCP element blank. Dawn and Horizon both position their hero media this way.


Anchor to Anti-pattern: inline background imageAnti-pattern: inline background image

{%- comment -%} Anti-pattern: don't do this {%- endcomment -%}
<section class="hero" style="background-image: url({{ section.settings.image | image_url: width: 2000 }});">
<h1>{{ section.settings.title }}</h1>
</section>

Anchor to Anti-pattern: external CSS background imageAnti-pattern: external CSS background image

/* Anti-pattern: don't do this */
.hero {
background-image: url("hero.jpg");
background-size: cover;
}

Both anti-patterns convert to the same markup. When the hero is a static section that always renders at the top of the page, the image is unconditionally eager and no section.index test is needed:

<section class="hero">
{{ section.settings.image
| image_url: width: 2000
| image_tag:
loading: 'eager',
fetchpriority: 'high',
widths: '600, 900, 1200, 1600, 2000',
sizes: '100vw',
class: 'hero__image'
}}
<div class="hero__content">
<h1>{{ section.settings.title }}</h1>
</div>
</section>

When a merchant can move the section anywhere on the page, use the section.index version in How instead. Both versions need the CSS from that section so that the image fills the section and the content stacks above it.


Use the Chrome DevTools Network panel to compare before and after:

  • Open the DevTools Network tab and throttle to Slow 3G to see the waterfall clearly.
  • Look at when the hero image request starts in the waterfall.
  • Background image: starts after the CSS file completes.
  • <img> tag: starts immediately during the HTML parse.


Was this page helpful?