Implementing Infinite Scroll in JavaScript
Learn how to create an efficient infinite scroll feature for your web applications
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
- 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);
-
Virtualize rendered items: For large lists, consider using virtualization libraries like
react-window
orvue-virtual-scroller
. -
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();
}
- 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.