← Back to snippets

Implementing Infinite Scroll in JavaScript

Learn how to create an efficient infinite scroll feature for your web applications

javascript
intermediate

Implementing Infinite Scroll in JavaScript

Infinite scroll is a popular UX pattern that loads content continuously as the user scrolls down, eliminating the need for pagination. This technique improves engagement by providing a seamless browsing experience.

Basic Implementation

Here's a simple implementation of infinite scroll using vanilla JavaScript:

let page = 1;
let isLoading = false;
const contentContainer = document.getElementById('content');

const loadMoreContent = async () => {
    // Prevent multiple simultaneous requests
    if (isLoading) return;
    
    try {
        isLoading = true;
        
        // Show loading indicator
        const loader = document.createElement('div');
        loader.className = 'loader';
        loader.textContent = 'Loading...';
        contentContainer.appendChild(loader);
        
        // Fetch the next page of content
        const response = await fetch(`/api/content?page=${page}`);
        
        if (!response.ok) {
            throw new Error(`HTTP error! Status: ${response.status}`);
        }
        
        const content = await response.json();
        
        // Remove loader
        contentContainer.removeChild(loader);
        
        // If no more content, stop listening for scroll
        if (content.items.length === 0) {
            window.removeEventListener('scroll', handleScroll);
            return;
        }
        
        // Append new content to the DOM
        appendContentToDOM(content.items);
        
        // Increment page for next fetch
        page++;
    } catch (error) {
        console.error('Error loading content:', error);
    } finally {
        isLoading = false;
    }
};

const appendContentToDOM = (items) => {
    const fragment = document.createDocumentFragment();
    
    items.forEach(item => {
        const element = document.createElement('div');
        element.className = 'content-item';
        element.innerHTML = `
            <h2>${item.title}</h2>
            <p>${item.description}</p>
        `;
        fragment.appendChild(element);
    });
    
    contentContainer.appendChild(fragment);
};

const handleScroll = () => {
    // Calculate when to trigger the next load
    // The "500" creates a buffer so content loads before user reaches the bottom
    const scrollPosition = window.innerHeight + window.scrollY;
    const threshold = document.body.offsetHeight - 500;
    
    if (scrollPosition >= threshold) {
        loadMoreContent();
    }
};

// Initialize the scroll event listener
window.addEventListener('scroll', handleScroll);

// Load the initial content
loadMoreContent();

Advanced Implementation with Intersection Observer API

A more modern and efficient approach uses the Intersection Observer API:

let page = 1;
let isLoading = false;
const contentContainer = document.getElementById('content');

const loadMoreContent = async () => {
    if (isLoading) return;
    
    try {
        isLoading = true;
        
        const response = await fetch(`/api/content?page=${page}`);
        const content = await response.json();
        
        if (content.items.length === 0) {
            // Remove the sentinel element if no more content
            const sentinel = document.querySelector('.sentinel');
            if (sentinel) sentinel.remove();
            return;
        }
        
        appendContentToDOM(content.items);
        page++;
    } catch (error) {
        console.error('Error loading content:', error);
    } finally {
        isLoading = false;
    }
};

const appendContentToDOM = (items) => {
    const fragment = document.createDocumentFragment();
    
    items.forEach(item => {
        const element = document.createElement('div');
        element.className = 'content-item';
        element.innerHTML = `
            <h2>${item.title}</h2>
            <p>${item.description}</p>
        `;
        fragment.appendChild(element);
    });
    
    // Insert before the sentinel element
    const sentinel = document.querySelector('.sentinel');
    contentContainer.insertBefore(fragment, sentinel);
};

// Create and append the sentinel element
const sentinel = document.createElement('div');
sentinel.className = 'sentinel';
sentinel.style.height = '1px'; // Invisible element
contentContainer.appendChild(sentinel);

// Initialize the Intersection Observer
const observer = new IntersectionObserver((entries) => {
    if (entries[0].isIntersecting) {
        loadMoreContent();
    }
}, {
    rootMargin: '0px 0px 200px 0px' // Load when sentinel is 200px from viewport
});

// Start observing the sentinel element
observer.observe(sentinel);

// Load the initial content
loadMoreContent();

Performance Optimization Tips

  1. Debounce the scroll event: When using scroll events, debounce the handler to prevent excessive function calls.
function debounce(func, wait) {
    let timeout;
    return function() {
        clearTimeout(timeout);
        timeout = setTimeout(() => {
            func.apply(this, arguments);
        }, wait);
    };
}

const debouncedHandleScroll = debounce(handleScroll, 100);
window.addEventListener('scroll', debouncedHandleScroll);
  1. Virtualize rendered items: For large lists, consider using virtualization libraries like react-window or vue-virtual-scroller.

  2. Clean up event listeners: Remove scroll listeners when components unmount to prevent memory leaks.

// Cleanup function for component unmounting
function cleanup() {
    window.removeEventListener('scroll', handleScroll);
    // Or for Intersection Observer
    observer.disconnect();
}
  1. Progressive loading with placeholders: Show loading placeholders that match the expected content dimensions to minimize layout shifts.

Infinite scrolling can significantly improve user engagement, but remember to implement it thoughtfully to avoid performance issues and provide accessibility options for users who prefer traditional pagination.