Lazy Loading
Do not miss this exclusive book on Binary Tree Problems. Get it now for free.
In this article, we have explored the idea of Lazy Loading in depth. It is an optimization technique to help webpages load faster.
Table of contents:
- Introduction
- Performance
- Code Splitting
- CSS Optimization
- Lazy-load images
- Lazy-load video
Introduction
Lazy loading is a method of identifying non-blocking/non-critical web resources and loading them when necessary. This can improve the performance of web pages by reducing rendering times and cache sizes.
Performance
Directly improving the performance of webpages is vital to improving user experiences, conversions, user retention, and scalability.
We often want to measure performance using RAIL:
- Response: time for completion of common processes after user actions.
- Animation: time for UI animations in response to user actions.
- Idle: time for deffered work during moments of no user interaction.
- Load: time to deliver interactive content.
Most companies will place Performance budgets laying out time constraints for the above metrics. Typically we want to reduce response, animation, and load times while maximizing idle times.
Lazy Loading is a method of improving load times by using available idle time to load non-blocking resources.
Lazy-loading usually occurs during user interactions such as scrolling and navigation.
Code Splitting
JavaScript, CSS and HTML can be split into smaller chunks. This enables us to send smaller packets of code improving page-load times. The rest of the application or webpage can be loaded on demand.
Two methods of code splitting are:
- Entry point splitting: separates code by entry point(s) in the app
- Dynamic splitting: separates code where dynamic
import()
statements are used in
JavaScript
We can also import javascript as ES6 modules, which have optimizations like default deferral. When loading javascript into html, the script
tag with type="module"
is treated as a JavaScript module.
<script src="filename.js" type="module"></script>
CSS Optimizations
CSS is a render blocking resource that, and is usually imported along with other stylesheets for purposes such as styling for print, different orientations, screen-sizes, and ui elements. By specifying a media
type, the browser will decide which stylesheet to load depending where the CSS needs to be applied.
<link rel="stylesheet" href="styles.css"> <!-- blocking -->
<link rel="stylesheet" href="print.css" media="print"> <!-- not blocking -->
<link rel="stylesheet" href="mobile.css" media="screen and (max-width: 480px)"> <!-- not blocking on large screens -->
Fonts
By default text rendering is defered for fonts. We can preload fonts using the following options:
<link rel="preload">
- the CSS
font-display
property - Font Loading API.
Lazy-Loading Images
Images typically contribute the most to data-usage and page load times. At most a few images are present in the viewport at a time, so we can use the following methods to detect when images enter the viewport to begin loading them.
The three primary methods are:
- Browser-level lazy-loading
- Intersection Observer
- Scroll, Resize, and OrientationChange event handlers
Browser-Level Lazy-Loading
The loading
attribute on an <img>/<iframe>
element can be used to instruct browsers to defer loading of images/iframes that are off-screen until the user scrolls near them.
<!-- for images -->
<img src="image.jpg" alt="..." loading="lazy">
<!-- for iframes -->
<iframe src="video-player.html" title="..." loading="lazy"></iframe>
The load event fires when the eagerly-loaded content has all been loaded; at that time, it's entirely possible (or even likely) that there may be lazily-loaded images that are within the visual viewport that haven't yet loaded.
You can determine if a given image has finished loading by examining the value of its Boolean complete
property.
let doneLoading = htmlImageElement.complete;
Intersection Observer
The Intersection Observer API allows users to know when an observed element enters or exits the browser’s viewport. This isn't available on older browsers, but offers a more efficient alternative to using event handlers in modern browsers.
An Intersection Observer can examine multiple elements at once and manage observing state changes, unlike event handlers which require you to specify what a state change is.
Assuming we have the following markup:
<img class="lazy"
src="placeholder-image.jpg"
data-src="image-to-lazy-load-1x.jpg"
data-srcset="image-to-lazy-load-2x.jpg 2x, image-to-lazy-load-1x.jpg 1x"
/>
There are four pieces of markup to focus on:
class
: the way we identify the img in JavaScript.src
: references a placeholder image that will appear when the page first loads.data-src
anddata-srcset
: these are placeholder attributes containing the URL for the image you'll load once the element is in the viewport.srcset
is used for responsive images, where the browser will load images at different resolutions automatically.
Normally the placeholder image is at a lower resolution so it can be loaded faster.
We can then define our Intersection Oberserver in the following JavaScript:
// When DOM Content is loaded do the following
document.addEventListener("DOMContentLoaded", function() {
// get all images with the class 'lazy'
var lazyImages = [].slice.call(document.querySelectorAll("img.lazy"));
// if the browser supports the IntersectionObserver API
if ("IntersectionObserver" in window) {
// Create a new IntersectionObserver
let lazyImageObserver = new IntersectionObserver(function(entries, observer) {
entries.forEach(function(entry) {
// for each image of class lazy in the viewport
if (entry.isIntersecting) {
// get the current image HTML element
let lazyImage = entry.target;
// load the correct image and other responsive image
// from the data attribute
lazyImage.src = lazyImage.dataset.src;
lazyImage.srcset = lazyImage.dataset.srcset;
// remove the lazy class from the image
lazyImage.classList.remove("lazy");
// remove the image from the observer (since it has been loaded)
lazyImageObserver.unobserve(lazyImage);
}
});
});
// for each 'lazy' image
lazyImages.forEach(function(lazyImage) {
// apply the observer function to the image (loads when image enters viewport)
lazyImageObserver.observe(lazyImage);
});
} else {
// Possibly fall back to event handlers here
}
});
The code above gives an overview of how to define an IntersectionObserver in comments identified by //
. In summary of the process:
- We load an array of lazy images
- Apply an observer to them
- Have the observer detect when an image is in the viewport
- Load the correct images, replacing the placeholder
- Remove the lazy image from the array of lazy ones and the observer from the loaded image
Event Handlers
Using the same HTML in the previous section, loading a set of images with the following attributes:
<img class="lazy"
src="placeholder-image.jpg"
data-src="image-to-lazy-load-1x.jpg"
data-srcset="image-to-lazy-load-2x.jpg 2x, image-to-lazy-load-1x.jpg 1x"
/>
We can create our own Observer using event handlers. This will also be backwards compatible and work on browsers that don't support the Intersection Observer API
// adds an event listener to run once DOM loads
document.addEventListener("DOMContentLoaded", function() {
// get all images with class 'lazy'
let lazyImages = [].slice.call(document.querySelectorAll("img.lazy"));
let active = false;
const lazyLoad = function() {
if (active === false) {
active = true;
// wait 200 ms before we begin loading lazy images in the viewport
// used to detect if a user is viewing the image or scrolling past it
setTimeout(function() {
// for each lazy image
lazyImages.forEach(function(lazyImage) {
// check if image is in viewport
let in_viewport = (
lazyImage.getBoundingClientRect().top <= window.innerHeight &&
lazyImage.getBoundingClientRect().bottom >= 0
)
// check if image is not visible
let is_not_visible = getComputedStyle(lazyImage).display !== "none"
// if the lazy image is in the viewport but not visible
if ( in_viewport && is_not_visible ) {
// replace the lazy image's placeholder with the correct one
lazyImage.src = lazyImage.dataset.src;
lazyImage.srcset = lazyImage.dataset.srcset;
lazyImage.classList.remove("lazy");
// remove all non-lazy (loaded) images
lazyImages = lazyImages.filter(function(image) {
return image !== lazyImage;
});
// remove the following event listeners once all lazy images are loaded
if (lazyImages.length === 0) {
document.removeEventListener("scroll", lazyLoad);
window.removeEventListener("resize", lazyLoad);
window.removeEventListener("orientationchange", lazyLoad);
}
}
});
// deactivate the loading function
active = false;
}, 200);
}
};
// have each event listener activate lazyLoad whenever a user:
// scrolls the screen
document.addEventListener("scroll", lazyLoad);
// resizes the screen
window.addEventListener("resize", lazyLoad);
// changes the screen orientation
window.addEventListener("orientationchange", lazyLoad);
});
Lazy-load Video
Compared to image data, video is easier due to the smaller quantity of video files uploaded with respect to images. Though video is considerably larger in terms of file size and can cause performance issues on sites hosting it.
We can use the same techniques as images, using:
- Browser-level lazy-loading
- IntersectionObserver API
- Event Handlers
This section will skip over using the IntersectionObserver API and Event Handlers implementation as they were showcased above and only require you to target <video>
HTML elements instead of <img>
HTML elements.
For browser-level lazy-loading, there are techniques we can use to avoid preloading a video to prevent load times:
<video controls preload="none" poster="one-does-not-simply-placeholder.jpg">
<source src="one-does-not-simply.webm" type="video/webm">
<source src="one-does-not-simply.mp4" type="video/mp4">
</video>
We can also use video elements to replace animated GIFs, since short videos usually will have smaller file sizes. We can enable GIF behavior with the following attributes:
<video autoplay muted loop playsinline>
<source src="one-does-not-simply.webm" type="video/webm">
<source src="one-does-not-simply.mp4" type="video/mp4">
</video>
The attributes above are self explanatory, except for playsinline
which is required for autoplay
to work in iOS.
With this article at OpenGenus, you must have the complete idea of Lazy Loading in Web Development.
Sign up for FREE 3 months of Amazon Music. YOU MUST NOT MISS.