Easy Endless Infinite Scroll With JavaScript

In this article, we’re going to learn how to easily implement infinite scrolling on a webpage using JavaScript.

What is infinite scroll?

Infinite scroll is a web design technique where more content is loaded automatically when the user scrolls down to the end. It removes the need for pagination and can increase the time users spend on our site.

Finished infinite scroll project

Our case study for this article will be a small project that demonstrates essential concepts related to infinite scroll.

Here it is:

HTML structure

Before looking at the JavaScript functionality, let’s check out the HTML markup for the project’s webpage.

HTML

<div id="load-trigger-wrapper">
  <div id="image-container"></div>
  <div id="load-trigger"></div>
</div>
<div id="bottom-panel">
  Images:
  &nbsp;<b><span id="image-count"></span>
  &nbsp;</b>/
  &nbsp;<b><span id="image-total"></span></b>
</div>

The #image-container div will contain the grid of images.

The #load-trigger div is observed by an Intersection Observer; more images will be loaded when this div comes within a certain distance of the bottom of the viewport.

The #bottom-panel div will contain an indicator of the number of images that have been loaded.

Detect scroll to content end

The detectScroll() function uses the Intersection Observer API to detect when the #bottom-panel div comes within a certain range of the viewport’s bottom. We set a root margin of -30px, so this range is 30px upwards from the bottom.

JavaScript

const loadTrigger = document.getElementById('load-trigger');

// ...

const observer = detectScroll();

// ...

function detectScroll() {
  const observer = new IntersectionObserver(
    (entries) => {
      for (const entry of entries) {
        // ...
            loadMoreImages();
        // ...          
      }
    },
    // Set "rootMargin" because of #bottom-panel height
    { rootMargin: '-30px' }
  );

  // Start watching #load-trigger div
  observer.observe(loadTrigger);

  return observer;
}

The callback passed to and Intersection Observer fires after the observe() call, so the images are loaded as the page is loaded.

Display skeleton images

Before the actual images are loaded, we first show a blank skeleton image with a loading animation. We store the image elements in an array variable to update them when their respective images have been loaded.

JavaScript

const imageClass = 'image';
const skeletonImageClass = 'skeleton-image';

// ...

// This function would make requests to an image server
function loadMoreImages() {
  const newImageElements = [];
  // ...

  for (let i = 0; i < amountToLoad; i++) {
    const image = document.createElement('div');

    // Indicate image load
    image.classList.add(imageClass, skeletonImageClass);

    // Include image in container
    imageContainer.appendChild(image);

    // Store in temp array to update with actual image when loaded
    newImageElements.push(image);
  }

  // ...
}

To display each image, we create a div and add the image and skeleton-image classes to it. Here are the CSS definitions for these classes:

CSS

.image,
.skeleton-image {
  height: 50vh;
  border-radius: 5px;
  border: 1px solid #c0c0c0;
  /* Three per row, with space for margin */
  width: calc((100% / 3) - 24px);

  /* Initial color before loading animation */
  background-color: #eaeaea;

  /* Grid spacing */
  margin: 8px;

  /* Fit into grid */
  display: inline-block;
}

.skeleton-image {
  transition: all 200ms ease-in;

  /* Contain ::after element with absolute positioning */
  position: relative;

  /* Prevent overflow from ::after element */
  overflow: hidden;
}

.skeleton-image::after {
  content: "";

  /* Cover .skeleton-image div*/
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;

  /* Setup for slide-in animation */
  transform: translateX(-100%);

  /* Loader image */
  background-image: linear-gradient(90deg, rgba(255, 255, 255, 0) 0, rgba(255, 255, 255, 0.2) 20%, rgba(255, 255, 255, 0.5) 60%, rgba(255, 255, 255, 0));

  /* Continue animation until image load*/
  animation: load 1s infinite;
}

@keyframes load {
  /* Slide-in animation */
  100% {
    transform: translateX(100%)
  }
}
The skeleton loading animation.
The skeleton loading animation.

Update skeleton images

Instead of getting images from a server, we get colors. After all the colors are loaded, we loop through the skeleton images, and for each image, we remove the skeleton-image class and apply the color.

JavaScript

function loadMoreImages() {
  // ...
  // Create skeleton images and stored them in "newImageElements" variable

  // Simulate delay from network request
  setTimeout(() => {
    // Colors instead of images
    const colors = getColors(amountToLoad);
    for (let i = 0; i < colors.length; i++) {
      const color = colors[i];
      newImageElements[i].classList.remove(skeletonImageClass);
      newImageElements[i].style.backgroundColor = color;
    }
  }, 2000);

  // ...
}

The getRandomColor() function takes a number and returns an array with that number of random colors.

JavaScript

function getColors(count) {
  const result = [];
  let randUrl = undefined;

  while (result.length < count) {
    // Prevent duplicate images
    while (!randUrl || result.includes(randUrl)) {
      randUrl = getRandomColor();
    }

    result.push(randUrl);
  }

  return result;
}

getColors() uses a getRandomColor() function that returns a random color, as its name says.

JavaScript

function getRandomColor() {
  const h = Math.floor(Math.random() * 360);

  return `hsl(${h}deg, 90%, 85%)`;
}

Stop infinite scroll

To save resources, we stop observing the load trigger element after all possible content has been loaded.

Let’s say we have 50 images that can be loaded and the loadLimit is 9. When the first batch of images is loaded, the amountToLoad should be 9, with 9 displayed images. When the fifth batch is to be loaded, the amountToLoad should still be 9, with 45 displayed images.

On the sixth batch, there’ll only be 5 images left to load, so the amountToLoad should now be 5, taking the displayed images to 50. This sixth batch of images will be the final one to be loaded, after which we’ll stop watching the load trigger element, with a call to the unobserve() method of the Intersection Observer.

So we use the Math.min() method to ensure that the amountToLoad is always correct. amountToLoad should never be more than the, and never less than the images left to load.

JavaScript

const imageCountText = document.getElementById('image-count');

// ...

let imagesShown = 0;

// ...

function loadMoreImages() {
  // ...
  const amountToLoad = Math.min(loadLimit, imageLimit - imagesShown);
  

  // Load skeleton images...
  // Update skeleton images...

  // Update image count
  imagesShown += amountToLoad;
  imageCountText.innerText = imagesShown;

  if (imagesShown === imageLimit) {
    observer.unobserve(loadTrigger);
  }
}

Optimize performance with throttling

If the user scrolls down rapidly, it’s likely that the Intersection Observer will fire multiple times, causing multiple image loads in a short period of time, creating performance problems.

To prevent this, we can use a timer to limit the number of times multiple image batches can be loaded within a certain time period. This is called throttling.

The throttle() function accepts a callback and a time period in milliseconds (time). It will not invoke the callback if the current throttle() call was made withing time ms of the last throttle() call.

JavaScript

let throttleTimer;

// Only one image batch can be loaded within a second
const throttleTime = 1000;

// ...

function throttle(callback, time) {
  // Prevent additional calls until timeout elapses
  if (throttleTimer) {
    console.log('throttling');
    return;
  }
  throttleTimer = true;

  setTimeout(() => {
    callback();

    // Allow additional calls after timeout elapses
    throttleTimer = false;
  }, time);
}

By calling throttle() in the Intersection Observer’s callback with a time of 1000, we ensure that loadMoreImages() is never called multiple times within a second.


function detectScroll() {
  const observer = new IntersectionObserver(
    (entries) => {
          // ...
          throttle(() => {
            loadMoreImages();
          }, throttleTime);
        }
      }
    },
    // ...
  );

  // ...
}

Finished infinite scroll project

You can check out the complete source code for this project in CodePen. Here’s an embed:

Conclusion

In the article, we learned the basic elements need to implement infinite scroll functionality using JavaScript. With the Intersection Observer API, we observe a load trigger element and load more content when the element gets within a certain distance of the viewport’s bottom. With these ideas in mind, you should able to easily add infinite scroll to your project, customized according to your unique needs.



Every Crazy Thing JavaScript Does

A captivating guide to the subtle caveats and lesser-known parts of JavaScript.

Every Crazy Thing JavaScript Does

Sign up and receive a free copy immediately.

1 thought on “Easy Endless Infinite Scroll With JavaScript”

Leave a Comment

Your email address will not be published. Required fields are marked *