On-Click Lazy Load YouTube Video Iframe in React

Last Updated:
Lazy load YouTube video iframe until click in React

Videos are heavy. And the JavaScript that is used in the video player is also heavy.

Speed matters on a website (you know this, its why you are here). And with most videos the JS for the player is generally not needed as soon as the page has loaded. In fact the only time the user needs the video player to be loaded is when they want to watch the it. I.E. At the point that the video is clicked.

I wrote a short piece on loading YouTube videos in React on scroll. However it didn’t consider waiting for the click – what if the video is above the fold? Or the user never watches the video?

Why load it if they don’t need it. Web.dev wrote an article on using loading=”lazy” with iframes which slightly helps – but some JS is still loaded.

The best result would be to have nothing but a compressed poster image shown until the user wants to watch the video. No matter if it is above or below the fold.

In this piece I will walk through how to defer an iframes load (and its contents) until a user had clicked a button.

 

TLDR

Render an image with the same ratio as the video iframe (YouTube, Vimeo etc) inside a button. When the button is clicked inject an iframe with the video which auto plays.

Take a look at this CodeSandbox for a working example of how to load a YouTube video on click in React.

I also put together a smaller more focused piece on lazy loading React components on click. May be helpful.

 

YouTube Without Improvements

YouTubes embed code with no alterations looks like this:

<iframe
    title="YouTube video player"
    src="https://www.youtube.com/embed/NY76mkzJT6o"
    width="1280"
    height="720"
    frameborder="0"
    allowfullscreen="allowfullscreen"
></iframe>

This music video by Fwar, a good friend of mine. If you like it you can find more of his music on Spotify.

This is fairly heavy. Lets work out what is needed to make this YouTube video lighter.

Requirements

The solution to this would look like:

  • Only a thumbnail is shown and loaded initially.
  • Thumbnail image should use browser lazy loading.
  • YouTube video iframe is deferred until user clicks thumbnail.
  • There is no flicker between thumbnail being removed and YouTube video showing
  • The video should play as soon as it had loaded.

Implementing the Solution

Firstly lets create a button and thumbnail to replace the iframe. This should be the same ratio as a YouTube video.

Also, lets use the YouTube play button icon to show the user they can interact with the button.

<button
  className={thumbnailButton}
  onClick={() => {
    startTransition(() => {
      setShowVideo(true);
    });
  }}
>
  <div className={videoInner}>
    <img
      alt="Fwar - Mushrooms video thumbnail"
      src="https://i.ytimg.com/vi/NY76mkzJT6o/maxresdefault.jpg"
      className={thumbnailImage}
      loading="lazy"
    />
    <img
      alt="Play Video"
      src="https://upload.wikimedia.org/wikipedia/commons/b/b8/YouTube_play_button_icon_%282013%E2%80%932017%29.svg"
      loading="lazy"
      className={playIcon}
    />
  </div>
</button>

Now lets add the classes needed for this section. This is where the ratio comes in. A little bit of padding magic!

For complete transparency, this is SCSS – it makes life a little easier. Take a look at the example to see how this fits together.

.container {
  max-width: 500px;
  padding: 10px;
}

.videoRatio {
  overflow: hidden;
  padding: 56.25% 0 0 0;
  position: relative;
  width: 100%;
}

.videoInner {
  bottom: 0;
  height: 100%;
  left: 0;
  position: absolute;
  right: 0;
  top: 0;
  width: 100%;
}

.thumbnailButton {
  @extend .videoInner;
  background-color: transparent;
  border: 0;
  cursor: pointer;
  display: block;
  margin: 0;
  padding: 0;
  text-decoration: none;
}

.thumbnailImage {
  width: 100%;
}

.playIcon {
  height: 42px;
  left: calc(50% - 30px);
  position: absolute;
  top: calc(50% - 21px);
  transition: all 0.3s ease-in-out;
  width: 60px;
}

Now this is set up, a new component is needed to hold the video. YouTube does have an API that can be used to handle autoplaying: https://developers.google.com/youtube/iframe_api_reference.
This is not used within this example as it seemed overkill. Instead lets use a package that wraps this API in a React component called react-youtube.

import YouTube from "react-youtube";
import { videoInner } from "./style.module.scss";

const Player = ({ setHasLoaded, videoId }) => {
  // Once the YouTube package (react-youtube) has loaded
  // tell the thumbnail it is no longer needed.
  // Play the video.
  const _onReady = (event) => {
    setHasLoaded(true);
    event.target.playVideo();
  };

  return (
    <YouTube
      videoId={videoId}
      onReady={_onReady}
      className={videoInner}
      iframeClassName={videoInner}
    />
  );
};

export default Player;

And that is it! See this CodeSandbox for the full working example! Hopefully this helped you, and if you have any questions you can reach me at: @robertmars

Related Posts

Helpful Bits Straight Into Your Inbox

Subscribe to the newsletter for insights and helpful pieces on React, Gatsby, Next JS, Headless WordPress, and Jest testing.