Lazy Load Animal Memes with Intersection Observer

By William Imoh

Last week we on the code challenge #7 we delved into lazy loading images for increased performance of web pages. Yet to take the challenge? You can do so here before looking through the spoiler below.

Once completed, you can post your entry in the comment section of the post, post it on twitter and use the hashtag #ScotchChallenge so we can see it, or post it in the #codechallenge channel of the Scotch Slack.

The Challenge

When developing websites and web pages, every byte counts and in a bit to reduce the page size and load time several techniques have been implemented to achieve this. Lazy loading is one such technique. Here, a much lower quality of an image is rendered on page load thereby reducing the overall size of the page and increasing load speed. However, when the page scrolls to the lower quality image, only then is the main (high quality) image loaded, hence ‘lazy loading’.

In this challenge, we will be lazy loading images of animal memes on a page using Intersection Observer. These images are sourced from Buzzfeed. We are provided with a base code used to quickly complete the challenge.

The Base

The base codepen consists of HTML and CSS code providing structure and style for the page. No JavaScript code was provided.


The HTML structure consists of a navbar, a div for the images and a scroll to top button. Bulma classes were specifically used to style these individual elements. Here is the parent div of each image:

<div class="box image is-5by4">
      <img src=",w_10/v1526593811/15_eegbn0.webp" data-src=",w_533/v1526593811/15_eegbn0.webp" alt="" id="top">

Each image is stored on the Cloudinary CDN and the URL dynamic transformation feature is used to serve a much-reduced version of the image initially. The data-src attribute is assigned the main image to be loaded when in the viewport.


As earlier stated, Bulma classes were used to style the HTML page, however, minimal styling was introduced to design each image card as well as the scroll to top button.

.container {
  width: 25%;

.image {
  margin: 30px auto;

.scroll-top {
  position: fixed;
  bottom: 10%;
  right: 5%;

The Technique

Techniques such as the use of getBoundingClientRect() or in-view.js would be cool to use, however a modern API shipped with the browser is the Intersection Observer API which allows us to provide an action triggered by the entrance of an element into the viewport or even a parent element.

The Intersection Observer API provides a way to asynchronously observe changes in the intersection of a target element with an ancestor element or with a top-level document’s viewport. – MDN

In this challenge, an Intersection Observer will be used to trigger a callback function once an image placeholder enters the viewport. This callback function fetches the higher quality image and replaces the placeholder.

Define Intersection Observer Options

First, we assign each element to be watched to a variable using the querySelectorAll() method. following that, we create the config options for the observer with:

const images = document.querySelectorAll('img');

const options = {
  root: null,
  rootMargin: '0px',
  threshold: 1.0

The root property holds a value of the element to be observed and setting it to nullimplies the observation of the viewport. As the name specifies, rootMargin is the margin set around the root element, this way you can create a small area within definite element dimensions to be observed.

threshold varies from 0 to 1 and is the visibility level of the image in the viewport before the callback function is triggered. A 0 means not visible, 0.25 means 25% visible, while 1 represents 100% visibility in the viewport.

Define Image Fetch and Update Function

To fetch the high-res image, we create a function which takes a parameter of the image to be loaded, utilizing a Promise, we create an image instance and assigns the src property of the image to the specified url parameter, while the onload and onerror properties are assigned the resolved and reject value respectively.

const fetchImage = url => {
  return new Promise((resolve, reject) =>{
    const newImage = new Image()
    newImage.src = url;
    newImage.onload = resolve;
    newImage.onerror = reject;

Next, we create a function to update the image once loaded. This is done by simply fetching the image with the fetchImage function created and dataset.src as it’s parameter. This way the high-res image is fetched and its src property is assigned to the parameter of the updateImage function created.

const updateImage = image => {
  let src = image.dataset.src;
    image.src = src

Now we have our higher quality images ready to be loaded.

Create The Callback Function

While we have the function to fetch the higher quality image, it is useless if it isn’t called on any entry image element (otherwise known as entries). We will proceed to create the callback function which receives the entries to the observer and the observer as parameters.

const callbackFunction = (entries, observer) =>{
  entries.forEach(entry =>{
    if(entry.intersectionRatio > 0){

The forEach loop simply loops through each observed entry, verifies that the element is currently visible in the viewport using the intersectionRatio property and calls the updateImage function on the target of the entry.

Create The Intersection Observer

We create the Observer instance and pass it the callback function and the config options as arguments.

const observer = new IntersectionObserver(callbackFunction, options)

Lastly, we apply the observer on the actual image elements using a forEach method:

images.forEach(img => {

Running the script the final product looks like:

I bet the fourth meme is pretty hilarious, lol.


So far we have implemented the lazy loading feature on the web page using the Intersection Observer API. This drastically reduced the load time of the page. Feel free to try this with more use cases including pop up modals and on an infinite scroll. Also, leave your comments and feedback in the comments section while we wait for the next challenge. Happy coding!