Image lazy-loading
What is lazy loading?
Images and other media files are usually large in size and contribute the largest part to a website's payload. When you visit a website that doesn't implement lazy-loading, the browser loads every single resource on the opened page, regardless of whether the user ever scrolls down to view such off-screen content.
The richer the content on the page, the more stuff will likely be loaded unnecessarily and the longer the user has to wait until the page loads. This is a big waste of resources in terms of data transfer, processing time, and memory usage. But it also affects the overall user experience and performance of your website, and therefore also hurts from a SEO perspective. Taken together, all these factors are ultimately bad for your business and for the environment.
Lazy-loading of resources, on the contrary, delays the loading of below-the-fold resources until they are actually needed, i.e., until the user scrolls near them, which results in a much smaller initial payload and improved performance.
Lazy-loading is therefore a best practice, especially on websites with a lot of (media) content. While we'll mainly focus on image loading in this recipe, it's worth noting that lazy loading can be applied to just about any resource on a page: HTML, stylesheets, JavaScript, images, videos, or iframes.
And the good news is that lazy-loading is pretty easy to implement. In the next sections, we'll look at our options.
For images, lazy-loading should always be accompanied by image optimization, the use of modern image formats, and a responsive image strategy to generally reduce the amount of data that needs to be loaded when requesting these resources.
How does lazy loading work?
When you visit a page, and as you begin to scroll down, at some point as you approach an image below the fold, usually a placeholder (e.g. a low quality image or solid color block) appears in the viewport, which is then replaced by the final image. Whether you will actually ever see the placeholder depends on multiple factors like the number of images that have to be loaded, your network speed or when the replacement actually takes place.
Until recently, the only way to implement a lazy-loading strategy was through JavaScript. While older libraries listen to scroll or resize events to determine if an element is visible within the browser's viewport, newer ones leverage the IntersectionObserver API.
With native support for lazy-loading images in modern browsers, we now have more options that can also be combined. So let's look at how to implement different options and their pros and cons.
Ways to implement lazy loading
There are several ways to implement lazy-loading on your website. So what are the decisive factors for preferring one solution over the other? They boil down to these:
- The browsers you need to support
- The type of resource you want to lazy-load
- The amount of control you need over the lazy-loading behavior
Browser-level lazy-loading
The easiest way to implement lazy-loading is by leveraging what is often referred to as "Native Lazy Loading", a browser feature which is supported in most up-to-date browsers at the time of writing this recipe, albeit with varying support (some browsers like Firefox currently only support images). Check out Caniuse.com for details.
The loading
attribute
To implement browser-level lazy-loading, use the loading
attribute with the value set to lazy
on the img
element. This attribute tells the browser when it should start loading the resource.
Simple example
Responsive images with srcset
With picture
element
When using the picture
element, the loading
attribute only needs to be set on the fallback img
element:
This works, because the image is displayed by the img
element, while the source
elements only provide the options the browser will choose from.
Browsers that don't support the loading
attribute will ignore it, so you can use it as progressive enhancement. There is also a polyfill avaliable or you use one of the JavaScript libraries we will introduce below as a fallback.
Loading attribute values
The loading
attribute supports three values that determine when a resource should be loaded:
lazy
: Used to defer fetching a resource until some conditions are met.eager
: Used to fetch a resource immediately; the default state.auto
(only Chrome, not part of the specification): Let the browser determine when to load the resource, same as not using the attribute at all.
When exactly a resource is loaded depends on the distance-from-viewport thresholds, which are not fixed and depend on browser engine and connection speed and may be subject to change in the future (see for example Google lazy-loading article.
Pros and cons
The charme of this approach is that you don't have to write custom code or add a JavaScript library. Additionally, browser-level lazy-loading works even if JavaScript is disabled client-side.
But there are also some downsides:
- Browser-level lazy-loading is limited to images and iframes (depending on browser support) and not supported by older browsers.
- Browser implementation determines the distance threshold, that is when exactly the resource is loaded, therefore you have no control over this behavior.
- Lazy-loading for background images is not supported.
Testing
Because of these thresholds, images are loaded rather early while they are still quite a bit out of the visible area, so they will usually be loaded when a user reaches a lazy-loaded image. Especially on a fast internet connection, you will therefore hardly be able to notice when exactly the images are loaded.
But you can check what is loaded and when if you open the network tab in your browser's developer tools. Create a page with multiple (full-width) images below each other, then open the page in the browser with the network tab open. Set the filter to only view images to make it easier. Then observe how images below-the-fold are loaded one by one as you scroll down.
It's also interesting to see how different browser engines behave and also how the behavior changes (for example in Chrome) when you enable network throttling.
For your convenience, I created a testsite on GitHub with some examples for the different techniques introduced in this recipe.
Lazysizes
One popular JavaScript library is Lazysizes. Lazysizes does not only support lazy-loading of resources like images, iframes, scripts, widget, etc. it also helps with implementing responsive images. The library has been around for 7 years and is under active development at the time of writing.
Setup
- Download the script and put it into
/assets/js
. - Load it using the
js()
helper function:Adapt the path to the file if you put it in another location.
You can include this script at the end of the body element before any blocking elements or in the head after any blocking elements, see as described in the documentation.
And that's it. The script doesn't need to be initialized but works by adding a class and data attributes to your markup, as we will see in the following examples.
Markup
For the most basic markup, you replace the src
attribute of an image element with a data-src
attribute:
Note that with this basic markup, users won't see any images if they have JavaScript disabled, therefore setting a low quality image with the src
attribute is a way to prevent that.
The downside of this approach is that you actually load two images, the low quality one and the final one.
An alternative could be to add a <noscript>
element with a link to the image instead of using a src
attribute:
If you modify the test page (see above) to use Lazysizes instead of browser-level lazy-loading and open the network tab in your browser's dev tools again, you can observe if and how the behavior is different from native lazy-loading.
Responsive images
Lazysizes also works with responsive images by replacing srcset
with data-srcset
. To make it really easy, we set data-sizes
to auto
to leave it to Lazysizes to do the calculations for us.
With data-sizes="auto"
, Lazysizes will make sure that the best-fitting image is used depending on how much screen-space the image is actually using.
Play around with the settings and observe the results in your developer tools.
Picture element
For the picture
element, add the lazyload
class to the img
element and use data-srcset on your source
and the img
element:
See the examples and the documentation of the library for more details and configuration options. There are also a few plugins available to lazyload other resources like background images, videos etc.
Lozad.js
Lozad.js is less than half the size of Lazysizes (1.25 kB minified & gzipped), and uses the IntersectionObserver API for lazy-loading. Since browser support for the IntersectionObserver API is more limited, you need a polyfill for browsers that don't support it.
Setup
-
Download the script and include it just before the closing body tag (adapt path as needed):
-
Then initialize the script:
If you want to reuse our previous Lazysizes examples without changing the class attribute, you can use a different selector when initializing the script:
Markup
The markup is similar to Lazysizes: you add a (configurable) class attribute with value lozad
and a data-src
attribute with the image url.
Simple image
Responsive images
Lozad also supports responsive images with the data-srcset
attribute just like Lazysizes:
However, since Lozad doesn't support auto-sizes like Lazysizes does, you will have to set sizes on the srcset options yourself if the images aren't used full-screen. We will cover responsive images in detail in a future recipe.
Picture tag
To make Lozad work with the picture
element, you have to set a minimum height and a display type other than inline
, otherwise the IntersectionObserver will not detect it:
Note that the img
element is not added.
The min height on the picture
tag should be set to a value that equals the actual height of the image. Setting it to 1rem
as in the example will likely result in all images being loaded at once. It might therefore make sense to set this value via JS.
A more modern approach would involve setting the aspect-ratio CSS property on the picture tag and then absolute position the img element inside it.
You can find all options and more examples in the documentation.
Vanilla lazyload
The third library I'd like to introduce here is Vanilla Lazyload. Compared to the other two mentioned above it's probably a lesser known. Like Lozad, it relies on the IntersectionObserver API, with a size of 2.4 kB minified and gzipped it takes a middle position. The library has extensive documentation and a lot of examples for different use cases. It supports images, animated SVGs, videos and iframes, responsive images and can enable native lazy-loading.
Setup
- Download and include the script before the closing
body
tag: - Initialize the script
Markup
The markup is the same as in the previous example in that you use the data-src
and data-srcset
attributes:
Simple image
Responsive images
Picture tag
And for the picture
element, you cadd a data-src
attribute on the img
element and a data-srcset
attribute on the source
element.:
As placeholder, you can again fall back to a a low quality image in the src
attribute of the img
element or use another placeholder strategy.
Library as polyfill for native lazy-loading
You can use any of the above libraries as fallback to native lazy-loading in unsupporting browsers. This works with the following pattern:
- Replace
src
attribute withdata-src
attribute and addloading
attribute on below-the-fold images: - Use a little script to replace the
data-src
attribute with asrc
attribute for browsers that support theloading
attribute, otherwise load the fallback library and initiate it.
Use with caution if you use placeholder images in a src
attribute. The loading
attribute interferes, since all data-src
attributes will be converted to src
once this script loads and would therefore overwrite all placeholder images. If the final images are loaded slower than the user scrolls, the user will see blank space instead.
Lazysizes also has a native loading extension that automatically uses native lazy-loading if available and otherwise falls back to the library's functionality.
Note that the same approach can also be implemented with the other libraries.
Lazy-loading: dos and don'ts
- It is generally recommended to avoid using lazy-loading on critical, above-the-fold content that should load as fast as possible. Since what is above-the-fold changes with the devices your website visitors are viewing your website/app with, it is a good idea to be rather generous when trying to determine this imaginary line. On a mobile device, maybe only the first image is in the viewport on first load, on a large desktop monitor, however, there might be quite a few critical images.
- Use a placeholder strategy for your images and videos to prevent layout shifts as content is loaded on demand.
- Images should include dimension attributes. Without dimensions specified, layout shifts can occur, which are more noticeable on pages that take some time to load. Layout shifts are not only bad from a user's experience point of view, but also for performance, because the browser has to recalculate the layout. Reserving space for the images can, for example, be achieved by setting a width and height attribute on the
img
element, or by setting theaspect-ratio
CSS property on a containing element and then absolute positioning the image inside.
Testsite
I created a little testsite with the examples in this recipe. While clicking through the different pages you can compare how the different approaches vary in how many images they load inititially and thus how the initial payload and loading speed differs.
The examples are all implemented with the default settings as in the examples in this recipe. For more options/config settings see the documentation of each library.
Conclusion
As we have seen, lazy-loading only requires very small changes to our usual markup and is therefore quickly implemented. The gain in performance, however, is huge, so there is really no reason at all not to do it.
If you can live with supporting modern browsers and your site is not very image heavy, it will probably suffice to implement native lazy-loading as progressive enhancement, ideally in conjunction with optimized images and modern image formats.
As a fallback for browsers that don't support native lazy-loading, or if you need more control over the behavior or want to support other resources like videos as well, use a lazy-loading library.