It got slower while I optimized it
An attribute that only helps when it's aimed, and the three places it makes things worse.
There’s a one-word attribute now that defers loading an image until it’s near the viewport. It’s supported in the browsers most of my visitors use, it needs no JavaScript, and it replaces a library.
So I put it on every image on a client site. Every one. It took 20 minutes, which included finding all the templates.
Then I measured it, because I’d spent the spring being sanctimonious about measuring things, and the main content was appearing later than it had before. Not dramatically. Consistently, on every page I checked, on a throttled connection.
The site had less to download and it was slower.
Later, not cheaper
Worth being precise, because the mistake follows directly from a detail people skim.
Normally the browser’s preload scanner finds an <img> while the HTML is still streaming in, before CSS has been parsed and long before layout has run, and it starts fetching immediately. That head start is one of the oldest and best optimizations browsers do, and it happens for free without anybody asking.
A lazily-loaded image opts out of that. The browser has to know whether the image is near the viewport, and it cannot know that until it has the CSS and has done layout. So the request doesn’t even get considered until much later in the load.
Which is exactly what you want for something 3,000 pixels down the page, and is strictly a loss for anything visible immediately.
Lazy loading doesn't make an image cheaper. It moves the decision later, which is a gift for an image nobody will see and a penalty for one everybody sees first.
One: above the fold
That’s the whole of my mistake, and it’s apparently the most common one going.
The hero image is usually the largest element on the screen, which means on most marketing pages it is the thing the loading metrics are measuring. Deferring it takes the single most important request on the page and moves it from “as early as physically possible” to “after CSS and layout.”
I’d taken a page’s headline visual and put it behind a queue.
The rule is easy and I now apply it without thinking: nothing above the fold is lazy. Not the hero, not the logo, not the first two cards in a grid. If it’s likely to be on screen at 0ms, it loads eagerly, and if it’s the biggest thing on screen you should probably be preloading it rather than merely not deferring it.
The awkward part is that “above the fold” varies by viewport, and a template doesn’t know. In practice I’ve settled for: the hero and the first row of anything, eagerly, everywhere. Slightly wasteful on a phone. Much better than the alternative.
Two: the JavaScript version, which is worse
Before there was an attribute, everybody did this with a script and a placeholder, and a great many sites still do, including two of mine until this spring.
<img src="placeholder.gif" data-src="/img/real.jpg" class="js-lazy" alt="…">
There is no image in that markup. There’s a string in an attribute the browser has no opinion about. So:
The preload scanner sees nothing to fetch, for every image on the page, including the hero, so the entire image loading process now waits behind your JavaScript bundle.
If the script fails, or a third party breaks it, or somebody’s blocker eats it, the page has no images at all. Not slow images. None.
And search engines and anything else reading the HTML see a placeholder. That’s a different conversation, and one I’ve had with an unhappy client.
The native attribute has none of those problems, because the real URL is in src where it belongs and the browser is making an informed decision rather than a script faking one. Where I still need the scripted version for browsers that don’t support the attribute yet, I do it the other way round: real src in the markup, and the script removes work rather than being required to do it.
Three: applying it to everything
The subtler one, and the reason the whole site got worse rather than just the hero.
Lazy loading has a cost per image: a check, a threshold, a deferred decision. On a 40-pixel avatar or a 2 KB icon that cost is a meaningful fraction of the saving, and on a page with 60 small images you’ve traded a handful of tiny parallel requests for 60 deferred decisions.
It also interacts badly with anything that changes layout. If an image without dimensions loads late, everything below it jumps, and you’ve now converted a loading problem into a visual stability problem. Worse, because it happens in front of the person rather than before them.
So dimensions on everything, always, and lazy loading only where there’s a genuine chance the image is never seen.
The shape of the mistake
I’ve made this same mistake in three other forms and I recognize it now.
Preloading everything, i.e. preloading nothing, plus contention: the browser now fetches 11 “critical” resources in parallel over a connection that can comfortably carry two, and each one arrives later than it would have.
Inlining all the CSS to remove a request, on a site where people visit four pages, so the same stylesheet is re-sent four times and the cache is never used.
Splitting assets across multiple domains, which was genuinely good advice a decade ago and became actively harmful once connections were reused, and which is still in a lot of documentation.
Every one of those is a real technique that helps in the situation it was designed for, applied uniformly, because applying something uniformly feels rigorous and takes 20 minutes. It’s the same instinct as putting !important on everything that isn’t working.
An optimization is a trade. If you can’t say what you’re giving up, you haven’t finished reading about it.
The lesson I’d actually keep
The fix took about ten minutes: eager on the hero and the first row, lazy on everything below, dimensions everywhere, and a preload on the hero. That’s faster than where I started, properly, measured, on a throttled phone.
The lesson I’d actually keep isn’t about images. It’s that I did 20 minutes of work, felt the satisfaction of having improved something, and would have shipped it and told the client it was faster. The measuring afterward was almost an afterthought and it was the only part that was worth anything.