What is lazy loading?
Lazy loading or loading content on demand is the process of identifying resources that are non-blocking to a website and delaying their loading or initialization until the page needs them. Common resources to lazy load include:
- Bundle JS files
- Vendor or third-party JS files
- CSS files
- Images
Reused image from AdobeStock
This article outlines the benefits of loading content on demand, how to do it, and when the technique should be applied. We’ll also share how we use lazy loading at Hotjar.
A quick caveat to manage expectations: in providing these tips, we assume all essential performance-related implementations already exist on your website and that, like us, your goal is to optimize page load time and Largest Contentful Paint (LCP) further. That said, let’s dive in.
Why is lazy loading important?
You’ve done everything right: content delivery network (CDN) set-up, file compression enablement, static JS and CSS file minification, and resource caching. You’ve been sure to follow good coding standards and promote the reusability of components. Now, you’re monitoring your website’s performance and hoping for the best.
But even after all this work, your page still takes too long to load.
You measure your loading time using a performance monitoring tool. As you suspected, it returns a poor score. But the problem is that none of your website resources can be removed—they're all crucial elements of your site in one way or another.
This is when lazy loading comes in.
Reused image from My Hero project
Four key benefits of lazy loading
- Reducing the initial web page load time by reducing the total size of resources downloaded
- Conserving the user's bandwidth, especially keeping in mind mobile data plans on mobile devices
- Conserving system resources, as requests to the server are made only when needed
- Avoiding unnecessary code execution
You might be wondering: don’t I need all the resources included in our website? Is there anything we could load on demand?
Answer: there's almost always something you don't need immediately.
Start by identifying the level of importance of the resources. Important resources might not be ideal to lazy load, such as:
- Main JS Bundle
- Main CSS Styles
- Fonts
- Analytics packages that need to be available on the initial website load
When to implement lazy loading
As your website grows, so does the final bundle of JS and CSS files. To use this technique effectively, you need to split the JS bundles and CSS files as much as possible. The traditionally bulky file holding JS, and another hefty file containing CSS, are no longer an option for the modern front-end tech world we live in. Once you’ve completed this critical step, you’re ready to reap the benefits of lazy loading.
Seven scenarios where lazy loading is beneficial:
- Loading JS bundles that aren't used on the currently viewed page but are used on other pages of the website
- Loading JS components and any library backing them up immediately instead of when that component is viewable on the page
- Using a third-party JS library which is only required on a specific page. Such libraries can be for ready-made features, JS utility libraries, animations, analytics, or monitoring purposes.
- Loading CSS-style files for the entire website rather than loading only the style files needed for the viewable page
- Loading a collection of images that aren't visible to the user until the user scrolls to a particular part of the page
- Loading of a resource when a specific DOM event has been triggered, such as resize or click
- At a specific state within your website, such as after submitting a form and showing a submission success component that might be animation-heavy
How Hotjar uses lazy loading in a React application
At Hotjar, we constantly strive to keep our website—our most important sales rep—at a solid performance score.
Why do we care so much about performance? Because it results in:
- Increased conversion rates
- More page views
- Higher search engine rankings
- Lower server bandwidth costs
- Smoother usability on different devices
Our website is a Next.js application. Thus, we rely heavily on the ReactJS library and its available components.
For images, we use the Next.js-provided component, which by default lazy loads automatically. This has meant we no longer have to worry that our website is suffering from overheads not displayed at that moment in time. Instead, images automatically load and display once the scroll position of a specific image reaches the browser's viewport.
Our milestone
When analyzing our resources on page load, Webpack Bundle Analyzer helped us identify a specific external library used by our website that was adding an overhead of 67.3kB on every page refresh. As you can see below, Webpack Bundle Analyzer’s output reveals lottie.js as one of the large libraries immediately downloaded on our page load.
(We also discovered this library was only used at the bottom of the page, to render a nice animation once a subset of components was in the viewport.)
Lazy loader implementation
Our idea was to create a new component (called LazyLoadOnScroll) to wrap other React components. The logic behind it uses a hook that benefits from the browser’s Observer tools, specifically the IntersectionObserver. Any components wrapped with this won’t be rendered, and any underlying external files, libraries, or JS files won’t be downloaded.
Once the user starts to scroll and this component is about to come into view, the IntersectionObserver returns a value of 'true', meaning the component is about to intersect the viewport. At this point in time of scrolling, everything begins downloading, and React renders the component accordingly, making it appear to the user as if items are seamlessly being served on demand.
**
The new files introduced are the following:**
- useIntersectionObserver: the hook with the responsibility of observing the provided component ref via an IntersectionObserver instance to return 'true' or 'false' as to whether the ref has intersected the viewport. This can also be extended by providing additional config props, but our use case required only the most basic implementation. This also includes a check to make sure the browser supports IntersectionObserver; otherwise, it will return 'true' to render the content immediately every time.
import { useState, useEffect } from 'react';
/**
* Providing a useRef to a component, this hook will return a value of true or false, based on whether
* the ref prop, is visible in the viewport or not
* @param ref
* @returns {boolean}
*/
const useIntersectionObserver = (ref) => {
const [isIntersecting, setIntersecting] = useState(false);
const supportsObserver = () => {
return typeof IntersectionObserver !== 'undefined';
};
useEffect(() => {
let observer;
if (supportsObserver()) {
observer = new IntersectionObserver(([entry]) => setIntersecting(entry.isIntersecting));
if (ref && ref.current) {
observer.observe(ref.current);
}
} else {
setIntersecting(true);
}
return () => observer?.disconnect();
}, [ref]);
return isIntersecting;
};
export default useIntersectionObserver;
- LazyLoadOnScroll: this is a React functional component that uses the useIntersectionObserver hook. It contains a statement checking whether the hook is being run on a server. If it is on a server, Next.js ‘Server Side Renderer’ will present the page to the client. The hook always returns 'true' so as not to lazy load anything, and the full page is generated for SEO purposes. When this component is initiated, it sets the children to render once the hook returns 'true'. The process occurs once, and the component does nothing else but keep the children in the rendered document.
import { useEffect, useRef, useState } from 'react';
import useIntersectionObserver from 'useIntersectionObserver';
/**
*
* @param props Child JSX element
* @returns {JSX.Element}
* @constructor
*/
export const LazyLoadOnScroll = (props) => {
const { children } = props;
const ref = useRef();
const isNotBrowser = typeof window === 'undefined';
const refIntersected = useIntersectionObserver(ref);
const [visible, setVisible] = useState(isNotBrowser);
useEffect(() => {
if (refIntersected) {
setVisible(refIntersected);
}
}, [refIntersected]);
return <div ref={ref}>{visible && children}</div>;
};
Putting it into practice
Using it is as simple as wrapping any component you want to lazy load. Plus, there’s no need to worry about extra overheads on page load.
<LazyLoadOnScroll key={key}>
<BigComponentToBeLazyLoaded prop1={} prop2={} etc... />
</LazyLoadOnScroll>
The beauty of measuring
For our performance measurement, we've used Chrome's devtool, Lighthouse, to generate a report before and after implementation. Results were fascinating, and I’m really excited to share the Desktop scores with you.
From an overall score of 87 on performance and an LCP of 1.9 seconds…
…we managed to go up to an overall score of 93 on performance and an LCP of 1.5 seconds.
These results gave us the assurance that loading content on demand does impact our overall website experience. Applying it further to other corners of our site will ensure that no unnecessary overheads block our pages from loading as fast as possible—helping us give our users an experience they’ll love.
Top comments (0)