Get NitroPack with up to 37% OFF
A common suggestion in Google’s PageSpeed Insights (PSI) is to “Defer offscreen images”.
Unfortunately, doing that is still more complicated than it can be.
At the same time, you shouldn’t ignore this opportunity, as lazy loading can immensely improve your website’s performance.
So, to learn more about:
Read on.
On the other hand, if you just want an easy way to lazy load all images, including background ones, skip to the end.
Lazy loading offscreen images refers to using a set of techniques to load only the images that visitors are currently looking at. Offscreen images aren’t visible before the user navigates to them. Deferring them makes sure they’re loaded after other, more critical resources.
But why is this even necessary?
Well, by default, before browsers start rendering, they request all images on the page, even those that aren’t immediately visible.
This is a problem, as images files tend to be heavy. In fact, they often contribute the most to page size, compared to other elements like CSS and JavaScript files.
According to the Web Almanac 2021, images remain the largest resource that contributes the most to the constantly growing page weight indicator:
Source - Web Almanac 2021
At the same time, visitors don’t need images they aren’t currently looking at. Without a person seeing it, requesting and processing any image is unnecessary.
This is where lazy loading comes in.
When applied correctly, this technique makes sure that:
Critical images (above the fold) are loaded instantly;
Non-critical images (offscreen) are loaded slightly before the visitor needs them.
In other words, if the user doesn’t scroll to an image, it’s never loaded.
As a bonus, lazy loading with properly sized placeholders can improve your site's perceived performance and Cumulative Layout Shift. More on that later.
Besides images, similar techniques can be applied to videos, JavaScript, and other assets.
Here’s an example of how lazy loading works:
As you can see, below the fold images are loaded slightly before the user scrolls to them.
You can also test this on our homepage. With lazy loading, the browser makes 23 initial image requests, only four of which are for images on our CDN.
Without lazy loading (add ?nonitro the URL), the same page triggers 43 initial image requests!
And while it can be difficult to implement by hand, lazy loading’s benefits far outweigh the downsides.
Deferring offscreen images has three crucial benefits:
Speed. Lazy loading reduces the initial page payload by cutting the number of images that need to be loaded upfront. As a result, browsers can render the page much faster;
Resource savings. Deferred images are only processed if the visitor needs them. This reduces the total number of bytes delivered on the page for users who bounce immediately or don’t scroll down. If your CDN provider charges based on data transfer and HTTP requests, lazy loading can literally save you money. The same is true for visitors on limited data plans;
Better resource utilization. Since some images are never loaded, lazy loading also saves resources like battery and processing time.
In short, lazy loading speeds up the initial page load time and helps utilize resources more efficiently.
There are three ways to lazy load images by hand:
Implement native (browser-level) lazy loading. The easiest option. Right now, it's supported by the most popular browsers (Chrome, Edge, Opera, Firefox). The implementation for Safari is still in progress.
Use the Intersection Observer. A bit more difficult as it requires JavaScript skills and experience with APIs. Works on all major browsers, except for Internet Explorer.
Use JavaScript event handlers. The most time-consuming way to do this. Works on all browsers. Mostly used as a fallback to the Intersection Observer method.
Each option has its pros and cons. I’ll cover all of them and provide links to in-depth tutorials.
Before we begin, note that images can be loaded through an img tag or invoked via CSS with a background-image property. This distinction has big practical implications, which I’ll touch on in a bit.
Back in 2019, Chrome introduced lazy loading at the browser level.
This was a big breakthrough.
Native lazy loading outsources all of the heavy lifting to the browser. And as you’ll see in the next sections, there’s quite a bit of lifting to be done.
With browser lazy loading, you only need to add a loading attribute with a value of lazy to the img tag in the HTML markup.
Put simply, all you need to do is tell the browser which images to lazy load. No JavaScript, APIs, or calculations required.
The best part is:
Because it's so simple, this method makes lazy loading accessible to everyone, not just web developers.
The loading attribute also supports two other values:
eager - for images that shouldn’t be lazy loaded;
auto - lets the browser decide if an image should be lazy loaded.
Unfortunately, this method also has some drawbacks.
Most importantly, it lacks browser support. At the time of writing this, Chrome fully supports it, but Firefox and Safari have partial and no support, respectively.
Source: caniuse.com. Note that the information from the screenshot can change over time. Always check the website for up-to-date info.
Similar to working with WebP Images, the lack of browser support makes lazy loading much harder than it needs to be.
Another drawback is that native lazy loading doesn’t work for background images, even in Chrome. For those, you still need to write a bit of JavaScript.
Some people have also criticized native lazy loading for being too eager.
Because of these issues, most people still don’t use this technique. As of February 2022, only around 20% of all images have a loading attribute.
Source - HTTP Archive’s state of images
If you want to learn more, Addy Osmany’s blog post goes in-depth about the intricacies of browser-level lazy loading.
The Intersection Observer API lets you detect when images come into view and take action as that happens.
With this method, you have to register an observer to images that should be lazy loaded. This requires writing a bit of JavaScript, but it's not nearly as tricky as using event handlers.
You can find a great example of the Intersection Observer in action by Rachel Andrew here. This is all the JavaScript from that example:
Here’s a quick breakdown of what’s happening:
All images have an attached class attribute named lazy;
There’s a src attribute referencing a low-quality placeholder image, which appears when the page initially loads. This prevents layout shifts and improves perceived performance. More on this later;
There are also data-src and data-srcset attributes referencing the images that should be loaded once the viewer needs them;
The isIntersceting property detects whether the image has entered the viewport. Once that happens, the image URL is picked up from the data-src/data-srcset attributes and moved to the src/srcset attributes, triggering the image load;
Finally, the observer and the “lazy” class are both removed.
Again, I highly recommend going over the example in detail.
The only drawback of using this method is that Internet Explorer doesn’t support it.
Source: caniuse.com. Note that the information from the screenshot can change over time. Always check the website for up-to-date info.
If most of your audience uses IE, you need to rely on JavaScript event handlers as a fallback.
On the other hand, if you’re okay with lazy loading on Chrome, Firefox, Safar, Edge and Opera, you can skip the next section.
For more details on how the observer works, refer to Mozilla's documentation.
I’m not going to cover this method in detail because it requires an entire article of its own.
Also, since we’re doing all the heavy lifting, this method opens the doors for a lot more mistakes. That’s why the Intersection Observer is far superior.
And once native lazy loading gains more traction, most sites will be using that anyway.
For now, here’s a quick summary:
You can use scroll, resize and orientationchange events to see if an image is in the viewport.
Check out this example from Rachel Andrew. It uses the same premise as the Intersection Observer app, but it does the job with plain JavaScript.
Again, when the img.lazy images enter the viewport, their URLs are picked up from the data-src/data-srcset attributes and moved to the src/srcset attributes.
Put simply, it’s very similar to using the Intersection Observer. We just have to write more JavaScript, including a timeout to throttle the lazy loading functions execution.
For a step-by-step guide, check out this article by CSS Tricks.
Also, you can try these lazy loading libraries to make your life easier:
As I said, images can also be invoked via CSS with a background-image property.
This makes them harder to lazy load, as the DOM, CSSOM, and render tree have to be built before the browser decides if the background image can be applied to a DOM element.
While complicated, this speculative behavior helps us “trick” the browser into not applying the background-image property to an element until it comes into view. That way, the element is never loaded until the user needs it.
Refer to this example from CSS Tricks for more details on how to do this.
It’s not that different from the previous methods. You still need JavaScript to detect when the element comes into view. The difference lies in the CSS.
Again, using the Intersection Observer with event handlers as a fallback (for IE users) is still the norm here.
Using placeholders for images and videos is a great way to improve actual and perceived performance.
Google, Facebook, and Medium all use a version of this technique.
Besides improved perceived performance, properly sized placeholders help the browser allocate enough space for each image. This prevents large layout shifts and bad CLS scores.
Now, there are a few ways variations you can try here:
The easiest way would be to use a neutral color box with the same dimensions as the image. It’s not the most elegant solution, but it works;
Another way is to find the dominant color of the final image and apply it to the placeholder. Manuel Timelthaler has a fantastic article on how to do this;
Finally, you can use a small, very low-quality version of the final image for the initial load. This low-quality image is similar to the final one, so the transition between both looks smoother.
It looks like Medium uses the third option. If you inspect their articles, you’ll find a small version (around 60x38px) of the final hero image:
This small image is rendered immediately, while the final one takes a bit longer. But because the colors and shapes of both versions are similar, it doesn't feel weird when the high-resolution image appears.
Again, when implemented correctly, these approaches also help reduce CLS. In the video below, Addy Osmani talks about optimizing CLS for an eCommerce store that uses placeholders for their product images:
The technique you choose here really depends on your preferences. Just remember to size placeholders properly, for example, with width and height attributes.
We just covered a lot of ground, so let's do a short recap:
Only defer offscreen (below the fold) images. Above the fold images should be loaded immediately;
Remember that desktop and mobile viewports have different above the fold elements. Take all devices into account when deciding which images to lazy load;
Consider the time and effort each lazy loading method requires. If most of your site’s visitors use Chrome, it may be okay to just add loading="lazy" to offscreen images and call it a day;
For 100% browser support, use the Intersection Observer with JavaScript event handlers as a backup;
Use properly sized placeholders to avoid CLS issues and improve perceived performance;
After deferring offscreen images, test to see if everything is working as it should. Use your browser’s network tab and scroll down the page to see when images are being loaded.
To avoid doing everything we just listed and still get the massive benefits of lazy loading, check out NitroPack.
Our service automatically lazy loads all images, iFrames, and videos from YouTube and Vimeo;
NitroPack also lazy loads all background images, even those defined in CSS. It detects declarations in external files multiple levels down an import chain. This is especially useful for certain WordPress themes;
NitroPack also lazy loads multiple slider images.
All of our lazy loading features are enabled by default. You don’t need to configure anything.
If you’re interested, check out NitroPack for:
We have a Free Plan (no credit/debit card required) for websites with up to 5000 monthly pageviews. You can easily test all of NitroPack’s features and see the results for yourself.
Evgeni writes about site speed and makes sure everything we publish is awesome.