loading...
Cover image for Lazy Load YouTube Video iFrame

Lazy Load YouTube Video iFrame

daviddalbusco profile image David Dal Busco Updated on ・5 min read

The Intersection Observer API is often used to lazy load images but did you know that it can be used to defer any types of elements?

This week I developed a new landing page for DeckDeckGo, our web open source editor for presentations, in which I’ll showcase some video. That’s why, for performance reason, I had to postpone their loading and why also, I’m sharing this new blog post.

Soundtrack

In this article we are going to lazy load a music video clip from my hometown friends Maxi Puch Rodeo Club. I could only highly advise you to play the following video in order to stream some great music while reading this blog post 😉

Getting Started

I implemented this experiment with React but the concept could be use with or without any frameworks. Before we actually defer the loading of the video, let’s add it to a component (I collected the iframe embedded code using the share action provided by Youtube).

import React, {} from 'react';

const Video = () => {
    return (
        <div>

            <div style={{'display': 'block',
                          'height': '2000px',
                          'background': 'violet'}}>
                Maxi Puch Rodeo Club
            </div>

            <div>
                <iframe 
                    width="560" height="315"
                    src="https://www.youtube.com/embed/ol0Wz6tqtZA"
                    frameBorder="0"
                    allow="accelerometer;
                           autoplay;
                           encrypted-media;
                           gyroscope;
                           picture-in-picture"
                    allowFullScreen
                    title="Maxi Puch Rodeo Club">
                </iframe>
            </div>
        </div>
    );
};

export default Video;

We can now open our browser and check that it is effectively loaded at the same time that our page. You will notice that the Youtube url is loaded even if the video is not displayed.

Obfuscate The Video

We create a new state to display or not our video. Per default, as we don’t want to load it when our page load, we set it to false.

const [showVideo, setShowVideo] = useState(false);

To defer the loading of the video, we are going to use the Intersection Observer API. It will detect if the element is (or going to be) visible in the viewport (if we don’t specify another root to observe). As soon as such a visibility is detected, it will triggers an event to let us perform a task, respectively to let us effectively load the video.

That’s why we are also wrapping our element in a container, because we do need an element to observe during the page lifecycle, regardless of the state of our video. Furthermore, we also create a reference to it in order to instantiate our observer later on.

import React, {createRef, useState} from 'react';

const Video = () => {

    const [showVideo, setShowVideo] = useState(false);

    const container = createRef();

    return (
        <div>

            <div style={{'display': 'block',
                          'height': '2000px',
                          'background': 'violet'}}>
                Maxi Puch Rodeo Club
            </div>

            <div ref={container}>
                {
                  showVideo ? <iframe 
                    width="560" height="315"
                    src="https://www.youtube.com/embed/ol0Wz6tqtZA"
                    frameBorder="0"
                    allow="accelerometer;
                           autoplay;
                           encrypted-media;
                           gyroscope;
                           picture-in-picture"
                    allowFullScreen
                    title="Maxi Puch Rodeo Club">
                  </iframe>: undefined
                }
            </div>
        </div>
    );
};

export default Video;

We can test our app in the browser, as we did previously, and should notice that the video is now neither loaded nor displayed.

Lazy Loading

Finally we can create our observer. The rootMargin is used to add a bounding box around the element to compute the intersections and threshold indicates at what percentage of the target’s visibility the observer’s callback should be executed.

const videoObserver = new IntersectionObserver(onVideoIntersection, {
    rootMargin: '100px 0px',
    threshold: 0.25
});

To instruct it to observe our container, we add a useEffect hook which will be executed according the container. Moreover, we also test if the browser do supports the API (which is supported currently by all modern platforms) and fallback on an “instant” load, if it would not be the case (“Hello darkness IE my old friend” 😅).

useEffect(() => {
    if (window && 'IntersectionObserver' in window) {
        if (container && container.current) {
            videoObserver.observe(container.current);
        }
    } else {
        setShowVideo(true);
    }

    // eslint-disable-next-line react-hooks/exhaustive-deps
}, [container]);

Finally, we declare a function which will be triggered when the container reaches the viewport. We use it to modify our state, in order to display the video, and to disconnect our observer, as we do not need it anymore.

function onVideoIntersection(entries) {
    if (!entries || entries.length <= 0) {
        return;
    }

    if (entries[0].isIntersecting) {
        setShowVideo(true);
        videoObserver.disconnect();
    }
}

Voilà, that’s it 🎉 We could perform our test again an notice that the video is only loaded when needed respectively when the container appears 😃

Going Further

Lazy loading is great but you might want also to add some custom control to play and pause your video. For that purpose, we can either code it by ourselves, with the YouTube Player API Reference for iframe Embeds, or use one of the many existing libraries, but, DeckDeckGo is open source and we do split our platform in multiple standalone components, therefore guess what? We do share a Web Component to embed easily Youtube video in your applications 😊

Let’s install it.

npm install @deckdeckgo/youtube --save

And load it in our application.

import { applyPolyfills, defineCustomElements }
         from '@deckdeckgo/youtube/dist/loader';

applyPolyfills().then(() => {
    defineCustomElements(window);
});

Then, we remove our state to display or not the video, because the Web Component won't load anything until further notice. We replace it with a new function called loadVideo in which we execute the component's method lazyLoadContent which takes care of everything.

async function loadVideo() {
    if (container && container.current) {
        container.current.lazyLoadContent();
    }
}

Finally, we add two buttons, used to call play and pause and we replace our iframe with the component <deckgo-youtube/>.

import React, {createRef, useEffect} from 'react';

import { applyPolyfills, defineCustomElements } 
         from '@deckdeckgo/youtube/dist/loader';

applyPolyfills().then(() => {
    defineCustomElements(window);
});

const Video = () => {

    const container = createRef();

    const videoObserver = new
        IntersectionObserver(onVideoIntersection, {
          rootMargin: '100px 0px',
          threshold: 0.25
        });

    useEffect(() => {
        if (window && 'IntersectionObserver' in window) {
            if (container && container.current) {
                videoObserver.observe(container.current);
            }
        } else {
            loadVideo();
        }

        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [container]);

    function onVideoIntersection(entries) {
        if (!entries || entries.length <= 0) {
            return;
        }

        if (entries[0].isIntersecting) {
            loadVideo();
            videoObserver.disconnect();
        }
    }

    async function loadVideo() {
        if (container && container.current) {
            container.current.lazyLoadContent();
        }
    }

    return (
        <div>

            <div style={{'display': 'block',
                          'height': '2000px',
                          'background': 'violet'}}>
                Maxi Puch Rodeo Club
            </div>

            <button onClick={async () => 
               await container.current.play()}>
                 Start
            </button>
            <button onClick={async () => 
               await container.current.pause()}>
                 Pause
            </button>

            <deckgo-youtube
               ref={container} 
               src="https://www.youtube.com/embed/ol0Wz6tqtZA">
            </deckgo-youtube>
        </div>
    );
};

export default Video;

We proceed with our final test, notice that the video is lazy loaded, we play with the buttons and we enjoy the awesome music of Maxi Puch Rodeo Club 🪕🥁🎵👍

Contribute To Our Project

Even if it does the job, our component can be improved. I notably think that a smoother transition to display the video would be useful. That’s why I opened a good first issue in our repo on GitHub. If you are up to give a hand, your help would be appreciated 🙏.

Cherry on the Cake 🍒🎂

Our component @deckdeckgo/youtube is a Web Component developed with Stencil and therefore it could be use in any modern web applications, with or without any frameworks. Moreover, if like me you tend to be a bit “bundlephobic”, it will add to your application, once minified and gzipped, only 198 bytes.

To infinity and beyond 🚀

David

Cover photo by Julia Joppien on Unsplash

Posted on by:

daviddalbusco profile

David Dal Busco

@daviddalbusco

Creator of DeckDeckGo | Organizer of the Ionic Zürich Meetup

Discussion

markdown guide
 

very useful, thanks! It solves my problem with google data studio charts !

 

happy to hear that Laurent!

note that I just read this week on Twitter that native lazy loading for iframe is about to happens. it might take a bit before all browser adopt the standard but good to know that something is happening.

twitter.com/zcorpan/status/1280977...