While making landing page for Hoverify, I noticed terrible loading times and performance. Scrolling was a laggy until everything got loaded. Since page is filled with videos, loading them at once was a very bad idea so I implemented some lazy loading with some pure JS. This article is for sharing what I learned.
What is lazy loading
Lazy loading is basically loading content at the point when it is needed. In our case we want to load videos when they are completely in the viewport.
Let's start
It is mostly javascript in action so there is not much on html and css side but we will start by writing a basic page for testing.
<html>
<head>
<title>Lazy Load Example</title>
</head>
<style>
video {
width: 540px;
height: 340px;
margin-bottom: 20rem;
display: block;
}
</style>
<body>
<!--Without lazy load-->
<video loop=1 muted=1 autoplay>
<source src="https://tryhoverify.com/videos/live_editing.mp4" type="video/mp4"></source>
</video>
<!--With lazy load-->
<video loop=1 muted=1 autoplay>
<data-src src="https://tryhoverify.com/videos/live_editing.mp4" type="video/mp4"></data-src>
</video>
<!--With lazy load-->
<video loop=1 muted=1 autoplay>
<data-src src="https://tryhoverify.com/videos/live_editing.mp4" type="video/mp4"></data-src>
</video>
</body>
</html>
As you can see there are three video tag. Top one is a normal video tag which will be normally loaded. Other two does not have <source>
tag because we will replace <data-src>
with <source>
later when the video is visible in the viewport.
Fun part
Now let's think about the problem for a second. We want to replace <data-src>
with <source>
whenever the video gets in the viewport for the first time. We also need to handle scroll, load and resize events as they will have effect on visibility of elements.
Detecting if the video is in the viewport or not
To do this we can use getBoundingClientRect()
to get the position of element on the screen and compare that to viewport size.
function isElementInViewport (el)
{
var rect = el.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
}
Listen to scroll, resize and load events
We will listen to load
, scroll
and resize
event and bind them to a handler function which will loop through all the videos found in the page. It will check if they are in the viewport and do our simple trick of replacing tags so that browser can start loading videos.
const handler = () =>
{
/*
Check videos if they are in the viewport.
If yes then replace data-src with source.
*/
}
addEventListener('load', handler, false);
addEventListener('scroll', handler, false);
addEventListener('resize', handler, false);
-
load
event is fired when all subframes, images, stylesheets, scripts, etc have been loaded. -
scroll
is fired whenever you scroll. -
resize
is fired when you resize the viewport.
Looping through videos
Last piece of puzzle is to handle all the events that we added above. As explained above we need to loop through and check if the videos are in the viewport or not.
const handler = () =>
{
const videos = document.querySelectorAll('video');
for (let i = 0; i < videos.length; i++)
{
const video = videos[i];
const visible = isElementInViewport(video); // Check if the video is in the viewport or not.
if (visible)
{
const dataSrc = video.querySelector('data-src');
if (dataSrc) // Check if data-src exists or not. If yes, than we have never loaded this video.
{
// Creating souce element and adding respective attributes.
const source = document.createElement('source');
source.src = dataSrc.getAttribute('src');
source.type = dataSrc.getAttribute('type');
video.appendChild(source); // Add new source element to video.
video.removeChild(dataSrc); // Remove data-src from video.
}
}
}
}
We replaced data-src
with video
when video was fully visible in the viewport. But you will see that we also placed a check to see if data-src
exists or not. This is because we are deleting data-src
when we replace it and it also saves us extra operations of replacing it again and again.
Final javascript code
function isElementInViewport (el)
{
var rect = el.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
}
const handler = () =>
{
const videos = document.querySelectorAll('video');
for (let i = 0; i < videos.length; i++)
{
const video = videos[i];
const visible = isElementInViewport(video); // Check if the video is in the viewport or not.
if (visible)
{
const dataSrc = video.querySelector('data-src');
if (dataSrc) // Check if data-src exists or not. If yes, than we have never loaded this video.
{
// Creating souce element and adding respective attributes.
const source = document.createElement('source');
source.src = dataSrc.getAttribute('src');
source.type = dataSrc.getAttribute('type');
video.appendChild(source); // Add new source element to video.
video.removeChild(dataSrc); // Remove data-src from video.
}
}
}
}
addEventListener('load', handler, false);
addEventListener('scroll', handler, false);
addEventListener('resize', handler, false);
You can find final working example here
This was a very basic example of lazy loading. You can do much better than this. For instance, you can add some animations or you can optimize event handling for better performance.
Please let me know if you find this useful in comments.
Top comments (0)