Lazy Loading With IntersectionObserver API

April 30, 2019


Hello 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');

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=”"></div>
<div><img data-src=”"></div>
<div><img data-src=”"></div>
<div><img data-src=”"></div>
<div><img data-src=”"></div>
<div><img data-src=”"></div>
<div><img data-src=”"></div>
<div><img data-src=”"></div>
<div><img data-src=”"></div>
<div><img data-src=”"></div>

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;

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 =
 target.setAttribute(‘src’, target.dataset.src)
}, {
 threshold: 0.5
images.forEach(image => observer.observe(image))

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).


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!

© Copyright 2021 Igor Irianto