A browser window showing six images that have loaded and three images waiting to load below the viewport.

Make native lazy-loading images load more eagerly

Firefox recently joined Chrome and added support for native lazy-loading images (NLLI). NLLI delays loading images until they’re needed to speed up page load times, and reduce the data needed to display just what’s immediately needed to display a page.

In an earlier article (the above link), I discussed how Safari and Firefox are too lazy (images load only as they become visible) and Chrome is too eager (images load way too early). I explained how lazy loading was implemented in browsers using the IntersectionObserver API, and how the rootMargin variable controlled the “eagerness”. In the context of lazy loading images, eagerness is the metric used to discuss when an out-of-view image is loaded.

There isn’t much to do about Chrome’s over-eagerness without abandoning NLLI entirely. It loads images thousands of pixels out of view; potentially wasting device resources and slowing down page load times. However, you can easily make Safari and Firefox a bit more eager so visitors won’t see empty areas as empty areas that should hold images are scrolled into view.

Before I go any further on this topic, I first want to urge you to ensure a better image loading experience for everyone by making good use of progressive JPEG images. Progressive JPEGs can display a partial image early on and fill in higher quality details as it’s being downloaded. This will probably make a bigger difference to more of your visitors than the topic in this article.

Modern image formats don’t support progressive loading. You may be using more modern formats like WebP, but a huge percentage of the web still uses JPEG for most images. Safari doesn’t support WebP so JPEG is still king at least among people using Apple’s browser.

But let me get back on track with lazy loading images. You can enforce a minimum preloading distance by setting up an IntersectionObserver with the desired rootMargin eagerness. The rootMargin specifies an element’s distance from the visible viewport before its downloaded.

I recommend using a rootMargin of about one–two viewport heights (vh). That’s the height the visible part of the webpage; or the content part of your browser excluding the chrome. The rootMargin property only supports pixel values, however. You can copy this value from the window.innerHeight property.

The following code is a complete implementation that can be used to enhance native lazy-loading images. It won’t override the browser’s default NLLI rootMargin, but it will help ensure a minimum eagerness.

if ("IntersectionObserver" in window)
{
  // get all the lazy images
  var lazy_imgs = document.querySelectorAll('img[loading="lazy"]');

  if (lazy_imgs.length > 0)
  {

    function intersection_handler(observed_imgs, observer)
    {
      observed_imgs.forEach (
        (ev) => {
          // image rootMargin is intersecting the viewport
          if (ev.isIntersecting)
          {
            // remove the observer
            observer.unobserve(ev.target);

            // image hasn’t loaded yet so load it now
            if (ev.target && !ev.target.complete)
            {
              ev.target.loading = "eager";
            }
          }
        }
      );
    }

    // calculate the desired rootMargins
    var rootVertical   = parseInt(window.innerHeight * 1.5);
    var rootHorizontal = parseInt(window.innerWidth * 1.5);

    let intersection_handler = {
      root: null,
      rootMargin: `${rootVertical}px ${rootHorizontal}px`,
      threshold: 0.0
    }

    // create the observer
    let observer = new IntersectionObserver(
      intersection_handler,
      C
    );
  
    // attach observer to images
    for (var img of lazy_imgs)
    {
      observer.observe(img);
    }
  }
}