loading...
Cover image for Best way to lazy load images for maximum performance
PROTOTYP

Best way to lazy load images for maximum performance

adrianbdesigns profile image Adrian Bece Updated on ・4 min read

Image lazy loading is one of the more popular approaches of optimizing websites due to the relatively easy implementation and large performance gain. With lazy loading we load images asynchronously, meaning that we load images only when they appear in the browser's viewport.

Almost a year ago, native lazy loading for images and iframes was released for Chrome and other major browsers. The point of the feature is to give browsers control when to request an image or iframe resource, which makes dev jobs a bit easier. Up to that point, only option was to use various JavaScript plugins which monitored the viewport changes and loaded resources dynamically. Now browsers can do that natively!

At the time of writing this article, around 73% of currently used browsers support this feature which is not bad, but we don't want to make the website image content inaccessible and unusable to 27% of potential users.

So this puts us in an interesting situation:

  • We want to use the benefits of native lazy loading for browsers that support it
  • We want to use a JS plugin as fallback for lazy loading for browsers that don't support it
  • We don't want to load the lazy loading JS plugin if the browser supports native lazy loading.
  • Support both img and source elements is mandatory

The "loading" attribute

We have three possible values that we can use for loading attribute.

  • auto - default value. Same as not setting the attribute.
  • eager - load the resource immediately.
  • lazy - load the resource once it's in the viewport.

Although it depends on the use-case, generally we want to use eager value for resources above the fold and lazy value for resources below the fold.

Modern approach

We need to write a script that will run after the HTML document is loaded. I've used Jekyll and added the script as an include that was appended to the end of the HTML body element. This is the most effective way of running JavaScript functions to avoid render blocking.

Image markup

We want our JavaScript function to start the image loading process based on the native lazy loading feature support. To achieve that we'll add the path to our images to data-src instead of src. But we shouldn't leave src empty, so we'll use 1x1px transparent image placeholder. Our markup for img elements will look something like this

 <img 
    src="/path/to/placeholder/image.png"
    data-src="/path/to/full/image.jpg"
    alt="Image description"
    class="lazyload"
    loading="lazy"
/>

Please note that class="lazyload" is used by the lazyload fallback plugin. I've used lazysizes that uses this particular class name.

Additionally, we want to support picture element that contains multiple source element and fallback img element.


<picture>
    <source data-srcset="path/to/image.webp" type="image/webp" />
    <source data-srcset="path/to/image.jpg" />
    <img loading="lazy" 
        class="lazyload"
        src="path/to/placeholder/image.png"
        data-src="path/to/image.jpg"
        alt="Image description"
    />
</picture>

Feature detection

We need to detect if user's browser supports native lazy loading. Luckily, we can do that using JavaScript directly.

   if ("loading" in HTMLImageElement.prototype) {
      /* Native lazy loading is supported */
   } else {
      /*  Native lazy loading is not supported */
   }

Final JavaScript code

For native lazy loading, we only need to assign data-src value to src value for img and source elements and let the browser handle the rest.

For unsupported browsers, we only need to load the JavaScript plugin and, optionally, run it (if not done automatically). I've used lazysizes but any plugin will work, just make sure that the markup is correct (class names, data elements, etc.).

So the final JavaScript code will look something like this:

<script>
    if ("loading" in HTMLImageElement.prototype) {
        var images = document.querySelectorAll('img[loading="lazy"]');
        var sources = document.querySelectorAll("source[data-srcset]");
        sources.forEach(function (source) {
            source.srcset = source.dataset.srcset;
        });
        images.forEach(function (img) {
            img.src = img.dataset.src;
        });
    } else {
        var script = document.createElement("script");
        script.src = "/link/to/lazyload.js";
        document.body.appendChild(script);
    }
</script>

Boosted performance & Lighthouse score

On my personal website I've used a JavaScript plugin for image lazy loading for all browsers. After implementing this modern approach, I've eliminated one JavaScript file that is being loaded and parsed on website load which in turn boosted my Lighthouse score and overall performance!

Alt Text

More image optimization techniques for maximum performance

Lazy loading is one of many ways to optimize image performance on the web. I've wrote this in-depth posts that covers other important techniques and aspects of image optimization for the web like web-specific image formats, using CDN, progressive images, etc.


These articles are fueled by coffee. So if you enjoy my work and found it useful, consider buying me a coffee! I would really appreciate it.

Buy Me A Coffee

Thank you for taking the time to read this post. If you've found this useful, please give it a ❤️ or 🦄, share and comment.

Posted on by:

adrianbdesigns profile

Adrian Bece

@adrianbdesigns

React, Frontend, Magento 2 certified developer. Magento PWA Studio contributor. Rock and metal music fan. Reads Dune, sci-fi novels and Calvin & Hobbes. Creates amazing interfaces @ prototyp.digital

PROTOTYP

A software company with a design first approach. We design, build, and ship app interfaces, native and web apps and interactive products for startups and enterprises.

Discussion

pic
Editor guide
 

Interesting approach with the "1x1px transparent image placeholder" Adrian, it gave me some idea 👍

If that would be interesting, in our eco system we also published a Web Component to lazy load images.

If will first try to use native lazy loading, if not supported by the browser, will fallback on the intersection observer technique and if not supported, will display the image instantly.

Also supports src-set, svg and it is possible to define an img to display in case of error (sh*t happens).

I had to check its source code to remember what I implemented (😅). Instead of a blank pixel I hide (opacity: 0) the image and when loaded toggle its opacity to render, kind of "smooth", transition.

Thanks again for the blog post, I might add a variable to set lazy (currently fix) or eager as you explained above.

 

Wow! Thank you very much. Glad you've found it useful. I haven't worked with Web components, I'll have to check them out at some point.

Regarding the 1x1 placeholder, you want an image to have a src set in any moment. I think that either the HTML validator throws an error or something breaks. Either way, it's semantically correct to have something there.

Cheers!

 

Thank you Adrian for the really clear add-ons explanation, it makes sense.

I have to give a try (again) to check how our Web Components handles this scenario, good point.

 

How does this allow the web crawlers to catalog things? Would you consider this something for ALL images / or just images in a infinite-scroll or otherwise web-app-ish type thing vs the core content of the web document?

In theory - you could load EVERYTHING but the document title and some h1's after page load and boost your score to 100%, right? ;) but then you'd have the same problem as a SPA.

 

Nice approach. I'll have to try this in my next project.

 

Using the with-preview Custom Element is also an option 😉

github.com/WebReflection/with-prev...

 

Quite interesting way. Did you consider to use Observable for lazy loading (e.g: loading only when the image must be shown/render?

 

Thank you. I went with native lazy loading (browser implementation) and avoid JS altogether for best performance and less dependencies. But for fallback JS plugin, you can use the Observable. I think that more popular plugins use them by default.