Lazy Loading Videos on Scroll with Vanilla JS and Intersection Observers
As frontend developers, we often encounter the challenge of optimizing website performance. One powerful technique to achieve this is lazy loading.
Lazy loading allows us to load assets, such as images or videos, only when they are needed, reducing initial page load times and conserving bandwidth.
I have written pieces on using React and Lazy Loading, but sometimes Vanilla JS is needed.
In this article, we’ll focus on lazy loading videos using the Intersection Observer API. By leveraging this built-in browser feature, we can efficiently detect when videos come into the user’s viewport and load them on demand.
TLDR
Take a look at this CodeSandbox for a working example of how to load a video on scroll.
Update
I recently wrote a piece on deferring the video until a user clicks. This is a more effective solution than waiting until scroll. It is about YouTube but could be refactored to work with most providers.
robertmarshall.dev/blog/on-click-lazy-load-video-iframe-in-react/
Setting up the HTML Structure
Before we delve into the JavaScript code, let’s understand the HTML structure we’ll be working with.
In the example, we’ll have multiple video elements, each representing a different video to be loaded on scroll.
<div>
<div
data-video-url="https://player.vimeo.com/video/588259728?h=09704145bb&dnt=1&app_id=122963&autoplay=1&muted=1"
data-video-title="This is an iframe title"
class="video-wrap"
style="
overflow: hidden;
padding-top: 56.25%;
position: relative;
width: 100%;
"
></div>
<div
data-video-url="https://player.vimeo.com/video/588259728?h=09704145bb&dnt=1&app_id=122963&autoplay=1&muted=1"
class="video-wrap"
style="
overflow: hidden;
padding-top: 56.25%;
position: relative;
width: 100%;
"
></div>
</div>
Each div
element contains styling to ensure the video’s correct aspect ratio and prevent page jumps when the video finally loads. The first div
includes data-video-url
and data-video-title
attributes, which we will later grab to inject into the iframe.
Section 2: The Intersection Observer API
Now to cover the Intersection Observer API. This is a built-in browser feature that allows us to observe changes in the intersection of elements with a viewport or a specific container.
The Intersection Observer is perfect for our lazy loading implementation because it can detect when a video element comes into view, signalling the browser to load the video.
Section 3: The JavaScript Implementation
First, we need to wait for the DOM to be fully loaded before starting our JavaScript implementation. We’ll use the DOMContentLoaded
event to ensure our script runs at the right time.
if (document.readyState !== "loading") {
initVimeo();
} else {
document.addEventListener("DOMContentLoaded", initVimeo);
}
Next, let’s define the initVimeo()
function, which will be the starting point of the lazy loading implementation. It will use the querySelectorAll method to select all video elements with the data-video-url
attribute.
function initVimeo() {
const videoWraps = document.querySelectorAll(".video-wrap[data-video-url]");
// More code will be added here later
}
Section 4: Implementing Lazy Loading
Inside the initVimeo()
function the Intersection Observer will check when each video element comes into view. If it’s the first time the element is visible (not yet loaded), it will to load the video dynamically.
function initVimeo() {
const videoWraps = document.querySelectorAll(".video-wrap[data-video-url]");
const videoStateMap = new Map();
const observer = new IntersectionObserver((entries) => {
entries.forEach(({ target, isIntersecting }) => {
if (isIntersecting && !videoStateMap.get(target)) {
const videoUrl = target.getAttribute("data-video-url");
const videoTitle = target.getAttribute("data-video-title");
const iframe = document.createElement("iframe");
iframe.src = videoUrl;
iframe.frameBorder = "0";
iframe.allow = "autoplay; fullscreen; picture-in-picture";
iframe.allowfullscreen = "";
if (videoTitle) {
iframe.title = videoTitle;
}
iframe.style = "position: absolute; inset: 0; width: 100%; height: 100%";
target.appendChild(iframe);
videoStateMap.set(target, true);
}
});
});
videoWraps.forEach((videoWrap) => {
videoStateMap.set(videoWrap, false);
observer.observe(videoWrap);
});
}
By leveraging the Intersection Observer API, we’ve successfully implemented lazy loading for videos, ensuring they load only when they are about to be displayed on the user’s screen. This optimization results in faster page load times and a smoother user experience.
To see a working version of this code, take a look at the Code Sandbox.
You can find me on Twitter if you have any questions.