HomeMethod

Knowledge

Web VitalsTechniquesSnippetsAnswersImpactsBottlenecks

Lazyload CSS Backgrounds

Defer CSS background image loading until the element nears the viewport using IntersectionObserver.

Each ring is a metric this technique moves.

Impact grade, A+ to D: how much of the Lighthouse score this technique can move. Unaffected: TBT, CLS, TTFB.

The Problem

Browsers discover and download background images referenced in CSS as they build the render tree. Large background images for off-screen sections are downloaded alongside critical above-the-fold resources, competing for bandwidth at exactly the wrong moment. This can delay First Contentful Paint and push back LCP on image-heavy pages.

Performance Impact

LCP

Medium

bandwidth freed from deferred backgrounds accelerates the actual LCP element

FCP

Medium

fewer competing requests during initial load allows critical resources to arrive faster

SI

background images outside the viewport do not compete with above-fold resources during initial visual loading

How it works

  1. 01

    Store the real URL in a data attribute

    The CSS rule for background-image is omitted or set to a placeholder colour. The actual image URL is stored on the element as data-bg-src. A class like lazy-background identifies it for the script.

  2. 02

    IntersectionObserver watches the element

    JavaScript registers an observer with a small rootMargin (e.g. 100px) so the browser monitors each target element. There is no scroll event and no polling — the browser calls back only when intersection state changes.

  3. 03

    Image URL is applied on intersection

    When the element enters the threshold, the script reads data-bg-src and applies it as an inline background-image style, triggering the HTTP request for the image.

  4. 04

    Observer is unregistered after load

    Once the background has been applied the element is unobserved, removing any ongoing overhead. The lazy-background class is replaced with lazy-background-loaded for state tracking.

HTML — data attribute markup

Code
<section class="hero-section lazy-background"
         data-bg-src="/wp-content/uploads/hero.jpg">
  <!-- content -->
</section>

CSS — placeholder while deferred

Code
.lazy-background {
  background-color: #f0f4f8;   /* placeholder colour */
  min-height: 250px;            /* reserve space — prevent CLS */
  background-size: cover;
  background-position: center;
}

JavaScript — IntersectionObserver

Code
document.addEventListener('DOMContentLoaded', () => {
  const targets = document.querySelectorAll('.lazy-background');

  const observer = new IntersectionObserver((entries, obs) => {
    entries.forEach(entry => {
      if (!entry.isIntersecting) return;
      const el  = entry.target;
      const src = el.dataset.bgSrc;
      if (src) el.style.backgroundImage = `url('${src}')`;
      el.classList.replace('lazy-background', 'lazy-background-loaded');
      obs.unobserve(el);
    });
  }, { rootMargin: '0px 0px 100px 0px' });

  targets.forEach(el => observer.observe(el));
});

Set rootMargin to start loading 100–200 px before the element scrolls into view, giving slower connections enough lead time to avoid a blank flash.

Limitations

  • Requires JavaScript — if the script fails or JS is disabled, backgrounds will not load. Provide a CSS fallback or <noscript> rule.
  • If a background image is visible on initial load — especially if it is the LCP element — lazy-loading it will delay its appearance and worsen LCP. Only defer off-screen backgrounds.
  • Without a placeholder colour and defined min-height, the section collapses to zero height until the image loads, which looks broken.
  • SEO crawlers that execute JavaScript will eventually discover the images, but CSS backgrounds carry less SEO weight than <img> alt text anyway.

Common Mistakes

Lazy-loading a hero background that is the LCP element

The LCP background should load immediately, ideally preloaded via <link rel="preload" as="image">. Reserve lazy-loading for sections that are genuinely off-screen at load.

Not setting min-height on the container

If the container has no content giving it height, it will be invisible (0px) and the IntersectionObserver will immediately fire — loading the image as if it were eager. Set a min-height matching the expected section size.

Using rootMargin: “0px” with no lead time

At 0px the image only starts loading when the element is already at the viewport edge. On slow connections this causes a visible blank flash. A 100–200px bottom margin gives enough lead time.

Test this techniqueImage DiagnosticsRun the tool

Need help applying this to your site?

Targeted fixes, not guesswork. We identify which techniques move the needle for your specific WordPress stack.

Start an Engagement
Support our family now