Scott Hanselman

Updating jQuery-based Lazy Image Loading to IntersectionObserver

April 11, 2018 Comment on this post [4] Posted in ASP.NET | HTML5 | Javascript
Sponsored By

The Hanselminutes Tech PodcastFive years ago I implemented "lazy loading" of the 600+ images on my podcast's archives page (I don't like paging, as a rule) over here https://www.hanselminutes.com/episodes. I did it with jQuery and a jQuery Plugin. It was kind of messy and gross from a purist's perspective, but it totally worked and has easily saved me (and you) hundreds of dollars in bandwidth over the years. The page is like 9 or 10 megs if you load 600 images, not to mention you're loading 600 freaking images.

Fast-forward to 2018, and there's the "Intersection Observer API" that's supported everywhere but Safari and IE, well, because, Safari and IE, sigh. We will return to that issue in a moment.

Following Dean Hume's blog post on the topic, I start with my images like this. I don't populate src="", but instead hold the Image URL in the HTML5 data- bucket of data-src. For src, I can use the nothing grey.gif or just style and color the image grey.

<a href="/626/christine-spangs-open-source-journey-from-teen-oss-contributor-to-cto-of-nylas" class="showCard">
    <img data-src="https://images.hanselminutes.com/images/626.jpg" 
         class="lazy" src="/images/grey.gif" width="212" height="212" alt="Christine Spang&#x27;s Open Source Journey from Teen OSS Contributor to CTO of Nylas" />
    <span class="shownumber">626</span>                
    <div class="overlay title">Christine Spang&#x27;s Open Source Journey from Teen OSS Contributor to CTO of Nylas</div>
</a>
<a href="/625/a-new-sega-megadrivegenesis-game-in-2018-with-1995-tools-with-tanglewoods-matt-phillips" class="showCard">
    <img data-src="https://images.hanselminutes.com/images/625.jpg" 
         class="lazy" src="/images/grey.gif" width="212" height="212" alt="A new Sega Megadrive/Genesis Game in 2018 with 1995 Tools with Tanglewood&#x27;s Matt Phillips" />
    <span class="shownumber">625</span>                
    <div class="overlay title">A new Sega Megadrive/Genesis Game in 2018 with 1995 Tools with Tanglewood&#x27;s Matt Phillips</div>
</a>

Then, if the images get within 50px intersecting the viewPort (I'm scrolling down) then I load them:

// Get images of class lazy
const images = document.querySelectorAll('.lazy');
const config = {
  // If image gets within 50px go get it
  rootMargin: '50px 0px',
  threshold: 0.01
};

let observer = new IntersectionObserver(onIntersection, config);
  images.forEach(image => {
    observer.observe(image);
  });

Now that we are watching it, we need to do something when it's observed.

function onIntersection(entries) {
  // Loop through the entries
  entries.forEach(entry => {
    // Are we in viewport?
    if (entry.intersectionRatio > 0) {

      // Stop watching and load the image
      observer.unobserve(entry.target);
      preloadImage(entry.target);
    }
  });
}

If the browser (IE, Safari, Mobile Safari) doesn't support IntersectionObserver, we can do a few things. I *could* fall back to my old jQuery technique, although it would involve loading a bunch of extra scripts for those browsers, or I could just load all the images in a loop, regardless, like:

if (!('IntersectionObserver' in window)) {
    loadImagesImmediately(images);
} else {...}

Dean's examples are all "Vanilla JS" and require no jQuery, no plugins, no polyfills WITH browser support. There are also some IntersectionObserver helper libraries out there like Cory Dowdy's IOLazy. Cory's is a nice simple wrapper and is super easy to implement. Given I want to support iOS Safari as well, I am using a polyfill to get the support I want from browsers that don't have it natively.

<!-- intersection observer polyfill -->
<script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=IntersectionObserver"></script>

Polyfill.io is a lovely site that gives you just the fills you need (or those you need AND request) tailored to your browser. Try GETting the URL above in Chrome. You'll see it's basically empty as you don't need it. Then hit it in IE, and you'll get the polyfill. The official IntersectionObserver polyfill is at the w3c.

At this point I've removed jQuery entirely from my site and I'm just using an optional polyfill plus browser support that didn't exist when I started my podcast site. Fewer moving parts means a cleaner, leaner, simpler site!

Go subscribe to the Hanselminutes Podcast today! We're on iTunes, Spotify, Google Play, and even Twitter!


Sponsor: Announcing Raygun APM! Now you can monitor your entire application stack, with your whole team, all in one place. Learn more!

About Scott

Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.

facebook twitter subscribe
About   Newsletter
Hosting By
Hosted in an Azure App Service
April 11, 2018 11:30
This is a really useful API!
Coding for the Web gets better and better.

Also learned that let is usable now, if you can get away with not supporting IE10 and only the most recent version of Safari (still using function scope totally defeated the purpose). https://caniuse.com/#search=let

I'd probably still wait a bit longer before using let on a website.
April 11, 2018 16:10
That dynamic polyfill from polyfill.io sounds fantastic, and is certainly more maintainable than trying to implement the dynamic polyfill yourself, but it seems like it would wreak havoc with a strong CSP. Sure, your CSP could white-list the specific site, but then if things from the site become compromised the door is open again. The typical solution is to add the hash of the script to the tag so the CSP can properly vet it, but dynamic scripts can't do that.
April 12, 2018 16:31
That's a lot of work for... nothing. Ok, this is totally a personal thing, but I absolutely hate lazy loading images. Why not just let the damn HTML rendering handle it? I get that this archive thing is a special case, but for normal pages it's not really saving any bandwidth as you still load the content, only at a later time - and there's actually an overhead with all the script cleverness, so you end up loading more! And it is totally annoying to try to read something when it's not "stable", keeps changing...

...yes, I am old ;-)
April 12, 2018 19:16
@El - you're assuming that the user is going to scroll down through the entire page. If that's the case, then yeah, this doesn't make a lot of difference in bandwidth.

But as far as page rendering speed, my understanding is that the page won't render until all the required resources (including the images) are present. By initially rendering with a placeholder (particularly, the same placeholder over and over), the page is able to render sooner. And if the images are styled to use the same dimensions regardless of whether it's the placeholder vs. the "for reals" image, then there shouldn't be any need to redraw the entire page.

And getting the page to render quickly is important for SEO. If Google thinks the page is slow enough to create a "bad" user experience, they'll drop the page in the rankings.

(Now if you say, "To heck with Google! Who put them in charge?" Well... I don't disagree with the sentiment, though making Google happy is, at present, a good move from a practical perspective.)

Comments are closed.

Disclaimer: The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.