Do you want to boost performance on your website? You can do that by using responsive images and lazy loading! In this article, you will find the HTML, JavaScript, and CSS code to lazy load responsive images, to make browsers use modern image formats like WebP and Jpeg2000, and to enable native lazy load where supported.

Lazy loading responsive images (2020)
Photo by Domenico Loia on Unsplash

Definitions

Responsive images are img tags that download the right image source depending on your design and the user’s device. You can provide information about your design in the sizes attribute and a list of image sources in the srcset attribute. You can also use media queries by wrapping your img in a picture tag. More about responsive images in the MDN.

Lazy loading images is a technique that makes your website render faster by deferring the loading of below-the-fold images to when they enter the viewport. Beyond performance, this also allows you to save bandwidth and money, e.g. if you’re paying a CDN service for your images.

Above-the-fold first

Bear in mind that using a script to lazy load images is a Javascript-based task and it’s relevantly slower than the regular image loading (eager loading from now on) which starts as soon as the HTML document is being parsed.

☝️ For this reason, the best practice is to eagerly load above-the-fold images, and lazy load only the below-the-fold images.

At this point, people usually ask:

💬 My website is responsive, how do I know how many images will be above-the-fold at page landing?

The answer is: count them! Open the web page in a browser, resize the viewport to the most common dimensions (smartphones, computers, and tablets) maybe using the device emulation tool, and count them.

If you can see 4 images above-the-fold in a smartphone viewport, plus only the tip of 4 more images on a desktop viewport, be conservative and eagerly load only 4.

Now to some code!

Here’s the HTML markup of an eagerly loaded responsive image.

<!-- Eagerly loaded,
     above-the-fold only -->
<img
  alt="Eager above"
  src="220x280.jpg"
  srcset="
    220x280.jpg 220w,
    440x560.jpg 440w
  "
  sizes="220px"
/>

And here’s the markup to lazy load a responsive image.

<!-- Lazy loaded,
     below-the-fold only -->
<img
  alt="Lazy below"
  class="lazy"
  data-src="220x280.jpg"
  data-srcset="220x280.jpg 220w, 
    440x560.jpg 440w"
  data-sizes="220px"
/>

Want to show a low-resolution preview while your lazy images are loading? You can do that by using a small, low-quality image in the src tag, like the following.

<!-- Lazy loaded,
     + low-res preview,
     below-the-fold only -->
<img
  alt="Lazy below with preview"
  class="lazy"
  src="11x14.jpg"
  data-src="220x280.jpg"
  data-srcset="220x280.jpg 220w, 
    440x560.jpg 440w"
  data-sizes="220px"
/>

Open the 👀 demo, then your browser’s developer tools, then switch to the Network panel. You will see that the first 2 images are eagerly loaded just after page landing, while the rest of the images are lazily loaded as you scroll down the page.

We’re using the img HTML tag and not the picture tag, given that the latter is not necessary in this case. I’ll dig into the picture tag use cases later in this article. ⏩ Skip to picture tag use cases

Script inclusion

To load the lazy images as they enter the viewport, you need a lazy load script such as vanilla-lazyload which is a lightweight (2.5 kb gzipped), blazing-fast, configurable, SEO-friendly script that I created and I’ve been constantly improving since 2014.

Here is the simplest way to include it in your page.

<script src="https://cdn.jsdelivr.net/npm/vanilla-lazyload@17.1.0/dist/lazyload.min.js"></script>

Have a look at the documentation for more ways to include LazyLoad in your web pages, like using an async script with auto-init, using RequireJS, using WebPack or Rollup.

LazyLoad initialization

Including the vanilla-lazyload script gives you a LazyLoad JS class you can use to load the images identified by the lazy CSS class. You must create a LazyLoad instance like this:

var lazyLoad = new LazyLoad({
  // Your custom settings go here
});

Minimize layout reflow

When using lazy loading, the images that haven’t started loading collapse to 0-height, only to grow when they’ll have started loading. Layout reflowing would make your website janky, so it’s a best practice to stabilize your layout by occupying the exact amount of space your images will take when loaded, before they start loading.

The universal solution to do that is to use the vertical padding trick, while in the future you’ll be able to use the aspect-ratio CSS directive to do it (as I’m writing it’s landed in Chrome Canary only).

.image-wrapper {
  width: 100%;
  height: 0;
  padding-bottom: 150%;
  /* ☝️ image height / width * 100% */
  position: relative;
}
.image {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
}

Here’s also a useful SASS mixin to do that (source: CSS tricks).

@mixin aspect-ratio($width, $height) {
  position: relative;
  &:before {
    display: block;
    content: "";
    width: 100%;
    padding-top: ($height / $width) * 100%;
  }
  > .content {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
  }
}

More info in Sizing Fluid Image Containers with a Little CSS Padding Hack by Andy Shora.

Avoid “broken” images

To avoid lazy images to appear as broken, even for a short amount of time, use CSS. Hide the images that still don’t have neither an src nor a srcset attribute set.

img:not([src]):not([srcset]) {
  visibility: hidden;
}

No polyfills required

You might be tempted to add one or more polyfills to support Internet Explorer (yes, I named it and it’s 2020). Don’t do that, you don’t need any. Let me tell you why:

  • Responsive images: Internet Explorer does not support responsive images, but you don’t need to use a polyfill because IE gracefully degrades using the image specified in the src attribute. So choose an image that would appear nice on a regular desktop display, place in the src attribute, and you’re cool.

  • IntersectionObserver: Internet Explorer does not support the IntersectionObserver API, which is used by vanilla-lazyload, but you don’t need to provide a polyfill because vanilla-lazyload will detect the support for that API and, if missing, it will loads all images immediately. This leads to the same result as if no LazyLoad was ever used on the page, but it doesn’t throw any errors.

That’s cool, Internet Explorer is not being used by more than 5% of the users today, and Microsoft is silently replacing it with Edge via Windows Update.

Anyway if for some reason you want it to work in the same exact way on Internet Explorer, you can use the IntersectionObserver polyfill by including it before vanilla-lazyload.

<!-- Don't do this if you're not sure! Read above -->
<script src="https://cdn.jsdelivr.net/npm/intersection-observer@0.10.0/intersection-observer.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vanilla-lazyload@17.1.0/dist/lazyload.min.js"></script>

Putting it all together

For your convenience here’s all the HTML, JS, and CSS code together.

<!-- Eagerly loaded,
     above-the-fold only -->
<img
  alt="Eager above"
  src="220x280.jpg"
  srcset="
    220x280.jpg 220w,
    440x560.jpg 440w
  "
  sizes="220px"
/>

<!-- Lazy loaded,
     below-the-fold only -->
<img
  alt="Lazy below"
  class="lazy"
  data-src="220x280.jpg"
  data-srcset="220x280.jpg 220w, 
    440x560.jpg 440w"
  data-sizes="220px"
/>
var lazyLoad = new LazyLoad({
  elements_selector: ".lazy"
  cancel_on_exit: true
});
/*
Images container to occupy space 
when the images aren't loaded yet
*/
.image-wrapper {
  width: 100%;
  height: 0;
  padding-bottom: 150%;
  /* ☝️ image height / width * 100% */
  position: relative;
}
.image {
  position: absolute;
  /* ...other positioning rules */
}

/*
Hide "broken" images before 
they start loading
*/
img:not([src]):not([srcset]) {
  visibility: hidden;
}

And that’s it for the simple img tag.

Picture tag use cases

Until now, I wrote about the img tag with the srcset and sizes attributes, which is the solution to the vast majority of the responsive images you might need to use on a website or web application. Now, in which cases should you use the picture tag?

Different width/height ratio

Use case: you need to show images with different width/height ratio depending on a media query. e.g. you want to show portrait images on mobile, vertical devices, landscape images on wider viewports, like tablets and computers.

Here’s the code you’re gonna need in this case. In order to have eagerly loaded images, just use the plain src and srcset attributes, without data- prefix.

<picture>
  <source
    media="(min-width: 1024px)"
    data-srcset="1024x576.jpg 1x,
      2048x1152.jpg 2x"
  />
  <source
    media="(max-width: 1023px)"
    data-srcset="640x960.jpg 1x,
      1280x1920.jpg 2x"
  />
  <img
    class="lazy"
    alt="Portrait or landscape"
    data-src="1024x576.jpg"
  />
</picture>

Open the 👀 demo, then your browser’s developer tools, then switch to the Network panel. You will see that it downloads only the image source corresponding to the first matching media query.

Load modern formats like WebP and Jpeg2000

Use case: you want browsers to choose the source to load a modern format like WebP and Jpeg2000 depending on its support for that format.

You need the source tag and the type attribute containing the MIME type of the images in the data-/srcset attribute.

<picture>
  <source
    type="image/jp2"
    data-srcset="1024x576.jp2 1x, 
      2048x1152.jp2 2x"
  />
  <source
    type="image/webp"
    data-srcset="1024x576.webp 1x, 
      2048x1152.webp 2x"
  />
  <img
    data-src="1024x576.jpg"
    data-srcset="1024x576.jpg 1x, 
      2048x1152.jpg 2x"
    data-sizes="1024px"
    alt="Jp2, WebP or Jpg"
    class="lazy"
  />
</picture>

Open the 👀 demo, then your browser’s developer tools, then switch to the Network panel. You will see that it downloads only the image source corresponding to the first type that your browser supports.

💬 Isn’t that markup too long for one image?

Yes, it is. And if you have money to invest in image optimization, there other ways to do that. Most of the cloud-based image servers in the market now automatically serve different image formats at the same URL. This means that you can request 1024x576.jpg and you get a WebP or a Jpeg2000 accordingly. Cloudinary and Akamai Image & Video Manager do that, amongst others.

Native lazyload

You might have heard or read of native lazy-loading coming to the web. Cool, isn’t it? As of May 2020, it’s supported in Chrome, Firefox, Edge, Opera, and behind a flag in Safari.

So 100% browsers support isn’t quite there, but in case you want to enable it on supported browsers, you could go for hybrid lazy-loading by setting the use_native option of vanilla-lazyload to true.

new LazyLoad({
  use_native: true
});

You might miss these features

If you go for native lazy-loading or hybrid lazyloading, you might miss some features that JS-driven lazy-loading grants.

  • automatic classes application on events (loading, loaded, etc.)
  • automatic retry loading images when the network failed and you’re back online
  • download cancelation when images exit the viewport while still loading, to prioritize the loading of new ones
  • callbacks on events triggered (viewport enter/exit, loading started/finished, etc.)

Think about it carefully before switching to native lazy-loading. If you don’t mind missing the above features, you’re good to go.


Conclusions

Here is a summary:

  1. Use vanilla-lazyload to load your lazy images.
  2. Don’t load all the images lazily, just the ones below the fold
  3. Use the img for simple responsive images
  4. Use the picture tag to
    • change your images width/height ratio at specific media queries
    • conditionally serve your images in modern formats like WebP or Jpeg2000
  5. Don’t use any polyfill if not strictly required

Happy lazy loading!

About this article

If something is unclear or you think it could be improved, let me know in the comments or tweet me.

☕ If you’ve found this useful, you might want to express your gratitude by buying me a coffee. ☕

Useful resources