DEV Community

Igor Irianto
Igor Irianto

Posted on • Edited on • Originally published at irian.to

Lazy Loading With IntersectionObserver API

lazy-kat

Hello dev.to folks! This is my first post - woot! Pretty excited to share what I have been learning recently about lazy-loading. Please let me know how I can make this better!

Lazy loading image is useful for loading page with many contents. We can easily find libraries to do that, such as yall.js and lozad.js. What most of these libraries have in common is they both use Intersection Observer API. Let’s learn how to use IntersectionObserver so we can understand how these libraries work — or even write our own!

First, I will briefly explain what IntersectionObserver does and second, how to use it to lazy load your own images.

What Does IntersectionObserver do?

(In layman’s words) IntersectionObserver asynchronously detects when an element intersects with ancestor element (usually viewport) and calls a callback function.

viewport schematics

Imagine a viewport containing images. When page loads, some images are positioned directly within viewport while some are sitting below viewport, waiting for user to scroll down so they can be seen. As user scrolls down, the top part of some lower-positioned images would eventually intersect with the bottom viewport. It is when the first top image pixel intersects with viewport the callback function loads the image.

Sample Usage

Let’s read the docs! Mozilla kindly gives us a starting point.

var options = {
  root: document.querySelector('#scrollArea'),
  rootMargin: '0px',
  threshold: 1.0
}

var observer = new IntersectionObserver(callback, options);
var target = document.querySelector('#listItem');
observer.observe(target);
Enter fullscreen mode Exit fullscreen mode

Above is the minimum setup to lazy load #listItem (technically options is optional, so var observer = new IntersectionObserver(callback); is a more concise way to run it).

Aight, let’s use it on a more realistic scenario. We are going to:

  1. Have 10 images in HTML that we will lazy load
  2. Add CSS fade animation
  3. Add IntersectionObserver to load images

HTML Setup

<div><img data-src=”http://placehold.it/300x300?text=1"></div>
<div><img data-src=”http://placehold.it/300x300?text=2"></div>
<div><img data-src=”http://placehold.it/300x300?text=3"></div>
<div><img data-src=”http://placehold.it/300x300?text=4"></div>
<div><img data-src=”http://placehold.it/300x300?text=5"></div>
<div><img data-src=”http://placehold.it/300x300?text=6"></div>
<div><img data-src=”http://placehold.it/300x300?text=7"></div>
<div><img data-src=”http://placehold.it/300x300?text=8"></div>
<div><img data-src=”http://placehold.it/300x300?text=9"></div>
<div><img data-src=”http://placehold.it/300x300?text=10"></div>
Enter fullscreen mode Exit fullscreen mode

If you notice, it does not use src but data-src attribute. One strategy for lazy loading is to start with HTML’s data-* attribute because data-src will not load the image.

CSS Setup

.fade {
 animation-duration: 3s;
 animation-name: fade;
}
@keyframes fade {
 from {
 opacity: 0;
 }
 to {
 opacity: 1;
 }
}
Enter fullscreen mode Exit fullscreen mode

This setup is optional. I think helps with our observation (plus it is more aesthetically pleasing) to have the image lazy load with fade animation.

Btw, you can check when the image is downloaded on network tabs if you use Chrome DevTool.

JS Setup

I want the images to load only when 50% of it intersects with viewport. This is how to set it up:

const images = document.querySelectorAll(‘img’)
const observer = new IntersectionObserver(entries => {
 entries.forEach(entry => {
 if(entry.isIntersecting) {
 const target = entry.target
 target.setAttribute(‘src’, target.dataset.src)
 target.classList.add(‘fade’)
 observer.unobserve(target)
 }
 })
}, {
 threshold: 0.5
})
images.forEach(image => observer.observe(image))
Enter fullscreen mode Exit fullscreen mode

I want to highlight a few things that I was struggling to understand when learning IntersectionObserver.

  • The argument entries represents all the image element under IntersectionObserver (I find it a bit odd having to iterate twice with images.forEach and entries.forEach, but that’s the way it is done). At initial page load, all entries are called. Some immediately intersects (if they are within viewports when page renders) while some don’t. The ones that immediately intersects have their callback function called immediately.

  • entry.isIntersecting returns true when the image intersects with viewport. Another common check for intersectionality is entry.intersectionRatio > 0.

  • As mentioned before, a common strategy for lazy-loading is to initially start without src. We transfer values from data-src to src right before user is about to see it.

  • It is good practice to unobserve the object after it has been loaded.
    We can change the amount or location of intersectionality with either threshold or rootMargin option. The ancestor element can be changed with root (default is viewport).

Conclusion

At the time of this writing, intersectionObserver is usable in major browsers except for IE. Check caniuse site for complete list.

IntersectionObserver is useful to lazy load element into viewport by passing the value from data-src into src upon callback. The same strategy can be applied to other elements.

Below are articles I read regarding IntersectionObserver I found useful (I am not affiliated with any of them, just appreciative of the information they gave and I hope it will help you too!)

Please feel free to let me know if you find any mistakes or how I can improve this. Thank you so much for reading this far. Y’all are awesome!

Top comments (3)

Collapse
 
anduser96 profile image
Andrei Gatej

Hi! Thanks for sharing!

As far as I’ve noticed, the number of entries depends on the number of thresholds you provide.

So, in this case, if we have threshold: 0.5, there will be only one entry.
I might be wrong. I’ll check as soon as I can.

Collapse
 
iggredible profile image
Igor Irianto • Edited

Hey Andrei! Thanks for reading! Appreciate the time you took to read. I believe the # of entries is the # of selection. Feel free to play with my codepen:

codepen.io/iggredible/pen/axQJwg

On JS, you can see that I console.log my entries. Right now I have 10 images. You'll see:

entries: (10) [IntersectionObserverEntry, IntersectionObserverEntry, IntersectionObserverEntry, IntersectionObserverEntry, IntersectionObserverEntry, IntersectionObserverEntry, IntersectionObserverEntry, IntersectionObserverEntry, IntersectionObserverEntry, IntersectionObserverEntry]

If you removed some of the images (say you removed 3, leaving you with only 7 img elements on HTML), you'll see:

entries:  (7) [IntersectionObserverEntry, IntersectionObserverEntry, IntersectionObserverEntry, IntersectionObserverEntry, IntersectionObserverEntry, IntersectionObserverEntry, IntersectionObserverEntry]

This is why I believe # of entries depends on # of elements you selected. The way I interpret it is, each entry is IntersectionObserverEntry object - I see it as properties of each image that are related to intersectionality. I think this is how IntersectionObserver keeps track of when each entry 'intersects' with viewports (or whatever ancestor you chose). As proof, you can see one of them having these attributes:

boundingClientRect: DOMRectReadOnly {x: 8, y: 22, width: 0, height: 0, top: 22, …}
intersectionRatio: 0
intersectionRect: DOMRectReadOnly {x: 0, y: 0, width: 0, height: 0, top: 0, …}
isIntersecting: false
isVisible: false
rootBounds: null
target: img
time: 425.7650000072317

I digress a little, but hope it helps!!

Collapse
 
anduser96 profile image
Andrei Gatej

Thanks for such a detailed and explanatory answer!
It definitely helped, now I have solved my misunderstanding.