Lazyload CSS Backgrounds
Defer CSS background image loading until the element nears the viewport using IntersectionObserver.
Metrics
This technique can move up to 33% of LCP's share of the Lighthouse score.
This technique does not move TBT.
This technique does not move CLS.
This technique can move up to 33% of FCP's share of the Lighthouse score.
This technique can move up to 33% of SI's share of the Lighthouse score.
This technique does not move TTFB.
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
01
Store the real URL in a data attribute
The CSS rule for
background-imageis omitted or set to a placeholder colour. The actual image URL is stored on the element asdata-bg-src. A class likelazy-backgroundidentifies it for the script.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.03
Image URL is applied on intersection
When the element enters the threshold, the script reads
data-bg-srcand applies it as an inlinebackground-imagestyle, triggering the HTTP request for the image.04
Observer is unregistered after load
Once the background has been applied the element is unobserved, removing any ongoing overhead. The
lazy-backgroundclass is replaced withlazy-background-loadedfor state tracking.
HTML — data attribute markup
<section class="hero-section lazy-background"
data-bg-src="/wp-content/uploads/hero.jpg">
<!-- content -->
</section>CSS — placeholder while deferred
.lazy-background {
background-color: #f0f4f8; /* placeholder colour */
min-height: 250px; /* reserve space — prevent CLS */
background-size: cover;
background-position: center;
}JavaScript — IntersectionObserver
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.
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