Lazy-load Videos in Next.js Pages

Banner for a MediaJam post

William Imoh

Over time, engineering efforts have been put into developing more performant web applications. Jamstack technologies like Next.js came from this need.

JAMstack technologies like Next.js emerged from an interest in improving the performance of modern web applications.

This post will show you one way to improve your application performance by lazy-loading video assets on a Next.js page.

Next.js is a modern React.js framework for building fast web pages and web applications. Next.js touts numerous features to improve both developer experience, and web application performance.

Lazy-loading is a technique employed in software engineering which requires the postponement of fetching/loading resources until the resource’s utilization is certain. The loading activity can be triggered by different user actions, including scrolling, hovering, or highlighting.

Lazy-loading is used to shave off any kilobytes contributing to the load time of pages or network resource utilization. It’s currently employed to defer images and JavaScript modules during runtime.

Now, as a page loads, videos load a placeholder image along with a portion of the video. Once the video is played, the remaining part of the video is buffered.

We’ll shave off the page kilobytes loaded by the video, and defer loading until the video is scrolled into view, using the web intersection observer API. React-intersection-observer is a React.js implementation for an intersection observer, which informs us when a DOM element is in view.


We completed this project in CodeSandbox, and you can fork it to run the code.

Prerequisites and installation

Having some knowledge of, and experience with, JavaScript and React.js is required for this project. The knowledge of Next.js isn’t a requirement, but it’s nice to have.

We install Next.js globally on our computer using yarn with the CLI command.

1yarn create next-app

This command opens a prompt to enter the project title. We use a project name of our choice, and yarn installs all dependencies required to bootstrap a Next.js project with the default starter.

Find all other methods to install Next.js here.

With the new project created, we’ll install two dependencies. We require the Cloudinary-React library to utilize the robust video player shipped by Cloudinary, and react-intersection-observer.

In the project directory, we install these packages using yarn in the CLI with:

1yarn add cloudinary-react react-intersection-observer

With these installed, we start a development server for the project using the CLI command, yarn dev.

Page creation

Like most modern frontend development frameworks, pages are created automatically using files in a designated folder. This is true for Next.js. The project’s home page is located in pages/index.js in the project’s root directory.

We need to create long-form content on the homepage so that we can sufficiently handle the scrolling activity. To do this, we make a content block component file titled ContentBlock.jsx with dummy text in src/components. In this file, we create a function component with the following:

1export const ContentBlock = ({ title = "Part" }) => {
2 return (
3 <div>
4 <h2>{title}</h2>
5 <p>
6 Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean nisl
7 nunc, commodo quis faucibus non, venenatis nec mi. Praesent velit neque,
8 gravida at felis a, pulvinar sollicitudin metus. Vestibulum quis
9 vehicula metus. Maecenas tincidunt est ex, nec volutpat enim gravida
10 vitae. Donec dui orci, lacinia nec suscipit vitae, suscipit eget nulla.
11 Pellentesque ipsum velit, accumsan vel vehicula et, convallis at eros.
12 Fusce ac elit id ante varius posuere vitae eget enim. Nunc enim ex,
13 sodales vitae suscipit eget, dictum ac ante. Pellentesque tincidunt
14 vehicula pellentesque. Aliquam posuere et turpis non tincidunt.
15 Curabitur rhoncus euismod ante euismod cursus. Donec quis arcu eu sem
16 mattis fringilla non et augue. Integer lorem purus, sollicitudin vel
17 egestas et, mattis efficitur metus. Nam consectetur faucibus sem varius
18 lacinia.
19 </p>
20 <p>
21 Duis vitae ultricies tortor. In hac habitasse platea dictumst. Praesent
22 a ante sit amet ante mollis mattis ac nec nibh. Ut id tristique leo.
23 Etiam lobortis lacinia purus at semper. In mattis tincidunt leo, non
24 laoreet neque elementum vel. Nam ullamcorper lacus massa, a finibus
25 neque auctor eget. Sed sapien felis, tempus quis tempor vel, ultrices eu
26 lorem. Aenean in egestas tellus. Proin velit ante, suscipit in dui sit
27 amet, maximus faucibus massa. Donec porttitor ante dolor, a convallis
28 ligula mollis id. Curabitur suscipit nisi nec velit ornare porta. In
29 erat odio, blandit et iaculis at, ultrices a sapien. Suspendisse
30 potenti.
31 </p>
32 </div>
33 );
34 };

This component receives a prop of title with which we set the content title, and it renders the set title and dummy text. We also added a default title of Part through the prop.

This content block component will be used multiple times on the home page to create a scrollable page.

In the home page component located in pages/index.js, we use the ContentBlock component to render multiple contents.

1import { ContentBlock } from "../src/components/ContentBlock";
3 export default function IndexPage() {
4 return (
5 <div>
6 <h1>Lazyloading videos in Next.js apps</h1>
7 <div>
8 <h2>We'll make a long page with text content</h2>
9 </div>
10 <ContentBlock title="Part 1" />
11 <ContentBlock title="Part 2 - After the video player" />
12 <ContentBlock title="Part 3" />
13 <ContentBlock title="Part 4 - After the video player" />
14 </div>
15 );
16 }

Video player component creation

We need to create a video player component which we use on the home page or any other page where it’s required. In src/components we create a new component file named VideoPlayer.jsx. In the video player file, first, we import the required dependencies and create a memoized video player component.

1import { Video, CloudinaryContext } from "cloudinary-react";
2 import { useState, useEffect, memo } from "react";
3 import { useInView } from "react-intersection-observer";
5 const MemoVidPlayer = memo(({ publicId }) => {
6 return (
7 <CloudinaryContext cloudName="chuloo">
8 <Video publicId={publicId} width="600px" controls />
9 </CloudinaryContext>
10 );
11 });

In the snippet above, we created a video player using the Video component in the Cloudinary-React package. This Video component accepts a prop of publicId, a unique identifier for the video on Cloudinary. This way, we can serve remote optimized images hosted on Cloudinary.

You can create a Cloudinary account to store, retrieve, and perform transformations on media assets, including videos. For this project, it is essential to note that we can replace the Video component used here with any other video element, including the HTML video element.

CloudinaryContext is a wrapper component that provides data passed as props to all of its child Cloudinary components. In this wrapper, we pass the cloud name of our Cloudinary account. You can obtain this from your Cloudinary dashboard after creating an account.

The video player is returned in a memo function to ensure that the component doesn’t rerender, so long as its content and data don’t change.

In VideoPlayer.jsx, we create an exported function that we will use on the home page. This function will handle the lazy loading of the previously created MemoVidPlayer component.

1export const VideoPlayer = ({ vidPublicId = "video-blog/cat" }) => {
2 const [publicId, setPublicId] = useState("");
3 const { ref, inView } = useInView({ threshold: 1 });
4 useEffect(() => {
5 if (inView === true) {
6 setPublicId(vidPublicId);
7 }
8 }, [inView, vidPublicId]);
9 return (
10 <div ref={ref}>
11 <MemoVidPlayer publicId={publicId} />
12 </div>
13 );
14 };

In the VideoPlayer component, we first created a state variable to store the video’s publicId. The video player will not load if the public ID is wrong or a source is not provided. This is a similar pattern used in lazy-loading images. We will update the source of the video once it scrolls in view.

Next, we destructured the ref and inView values from the useInView hook imported from react-intersection-observer. The ref value is assigned to the DOM element to be tracked, and inView returns a boolean value to show when the tracked element is in view. The threshold option ranges from 0 to 1, showing how much of the tracked element needs to be in view to trigger a change in the inView value.

In a useEffect hook, we check if inView is true before updating the state value from an empty public ID to the video’s specified public ID. The useEffect hook takes a dependency array of values that would trigger the component’s rerender if it changes.

For our VideoPlayer component, we set a default public ID of video-blog/cat.

Lastly, we update the home page component to show the video player, with each video having a different public ID from the other.

1import { VideoPlayer } from "../src/components/VideoPlayer";
2 export default function IndexPage() {
3 return (
4 <div>
5 <h1>Lazyloading videos in Next.js apps</h1>
6 <div>
7 <h2>We'll make a long page with text content</h2>
8 </div>
9 <ContentBlock title="Part 1" />
10 <VideoPlayer />
11 <ContentBlock title="Part 2 - After the video player" />
12 <VideoPlayer vidPublicId="video-blog/kitten" />
13 <ContentBlock title="Part 3" />
14 <VideoPlayer vidPublicId="video-blog/doggo" />
15 <ContentBlock title="Part 4 - After the video player" />
16 </div>
17 );
18 }

Here’s how the final page looks.

We also check the browser console to see the loaded media assets in the network tab. The network tab shows the deferred loading of the video thumbnail, and preloaded video.


This post discussed the importance of lazy-loading media assets for performance improvements on modern web pages. Also, we discussed how to lazy-load a video component on a page using the react-intersection-observer package. Experiment with lazy-loading multiple videos on a page for improved performance benefits.

You may find these resources useful:

William Imoh

Creating tech solutions and talking about them

William is a developer, developer advocate, and product manager. When he's not working on technology, he's organizing game nights, making drinks, or playing basketball.