Skip to article body
A young bread carrier balancing a wooden tray of flat bread on his head against a dusk sky
A young bread carrier balancing a wooden tray of flat bread on his head against a dusk sky

Web Development · Field Notes

A CSS Mask Downloads Before Anyone Scrolls

loading="lazy" does nothing for an image you reference from CSS. Our client logos were downloading on first paint.

By Shehab Waleed, Founder, Cairo Studio 2 min read

The client logos on our homepage are drawn with CSS masks. Each logo is a mask over a solid block of colour, so the logos take the text colour and switch with the theme without a second set of files.

<span
class="bg-title mask-contain mask-center mask-no-repeat"
style:mask-image={`url(${logo})`}
></span>

During a performance pass, those logos showed up among the first requests on the homepage, at full size, even though the strip sits far below the first screen.

CSS images do not wait

An img element with loading="lazy" waits until it is near the viewport. A CSS image has no such option. The browser requests it when it styles and renders the element, and position on the page plays no part.

We checked this in headless Chromium with a mask placed 5000 pixels below the top. Without any scrolling, the mask file was requested. A lazy img element in the same spot was not.

Set the mask late

The fix is to leave the mask out of the markup until the strip is close. An IntersectionObserver with a generous margin flips one flag, and the style only exists after that.

const observer = new IntersectionObserver(
(entries) => {
if (!entries.some((e) => e.isIntersecting)) return;
observer.disconnect();
near = true;
},
{ rootMargin: '100% 0px' }
);
style:mask-image={near
? `url('${sized(logo, 640)}')`
: undefined}

A margin of 100% means the logos start loading one full screen before the strip arrives, so a normal scroll never meets an empty mask.

Ask for the size you draw

The second half of the fix was size. The masks pointed at the original uploads, some of them large. Prismic serves images through imgix, so a width parameter returns a smaller file. The grid now asks for 640 pixels and the marquee for 320.

const sized = (url, w) =>
`${url}${url.includes('?') ? '&' : '?'}w=${w}`;

A third step had the same problem: a script sampled every logo at 96 pixels on load to decide how to frame it. That now waits for the same flag.

We shipped this with several other loading fixes on the same day, so we cannot give you a separate number for it. It is the same lesson as our five copies of one font: read the request list, and question everything on it that the first screen does not use.

Share
X LinkedIn Facebook WhatsApp Email
Bakers in white carrying stacked wooden bread trays, one hand on the top tray

Delivered before anyone asked.

img with loading="lazy"
Near the viewport
CSS mask or background
As soon as the element renders
img with loading="lazy"
srcset and sizes
CSS mask or background
One URL you choose
img with loading="lazy"
Built in
CSS mask or background
Set the URL from script when needed
img with loading="lazy"
No
CSS mask or background
Yes, as a mask over a colour

No. The browser requests a CSS image when it renders the element, wherever the element is. To delay it, add the image URL from script when the element comes near the viewport.

No. The loading attribute only works on img and iframe elements. For anything drawn with CSS, delay the URL itself.

A single colour logo used as a mask takes the current text colour, so it follows light and dark themes without separate files.

We audit and fix real loading problems, measured on the devices your visitors use.

See how we work on speed

More from the Journal

view all →