This article explains how to lazy load a YouTube iframe component in React.
This article was originally posted (and is more up to date) at https://robertmarshall.dev/blog/lazy-load-youtube-video-iframe-show-on-scroll/
I recently added a YouTube video to a clients Gatsby website and found that it had a massive hit on performance. The below code is a solution to that problem!
YouTubes Embed Code with No Alterations
If you copy the YouTube embed code, this is what it looks like:
<iframe
title="YouTube video player"
src="https://www.youtube.com/embed/M8m_qKKGk2U"
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.
No suggestion or indication of lazy loading seems to be factored in. This surprised me as Chrome has rolled out a lazy
attribute that defers loading of offscreen iframes and images until a user scrolls near them (more information on that here). I tried adding this and it increased performance slightly. However the YouTube scripts for the player were still being loaded too soon causing a performance hit.
We need to completely defer all YouTube related scripts loading until the user actually needs them.
Using the Intersection Observer API To Lazy Load Iframes
The Intersection Observer API is often used to lazy load images, Gatsby’s new fancy Gatsby Plugin Image is a perfect example of this. But it can also be used to load all manner of other things. Why not use it to load YouTube iframes as well…
I initially considered building out the whole intersection observer functionality myself, but when digging a bit deeper I found that there were a number of polyfills and other magic needed to support edge cases. Not wanting to re-invent the wheel I decided to use the useIntersectionObserver()
hook provided by Jared Lunde from his brilliant React Hook package.
How To Use useIntersectionObserver()
The thing I love most about hooks is that they are generally broken down into a single use, super easy to use function. This hook is no exception to that rule. Using the hook is as simple as importing it from the package, and plugging it in.
import { useState } from 'react'
import useIntersectionObserver from '@react-hook/intersection-observer'
const Component = () => {
const [ref, setRef] = useState()
const { isIntersecting } = useIntersectionObserver(ref)
return <div ref={setRef}>Is intersecting? {isIntersecting}</div>
}
Adding the Intersection Observer Functionality to the Iframe in a Component
When I first plugged in the Intersection Observer hook into the iframe I noticed it hid and showed itself as I scrolled up and down the page. This is because the observer was working as it should do and only showed the component when it was on the screen. I changed the useState
in the example to a useRef
, and added a conditional to make sure it was shown and locked.
import { useRef } from 'react'
import useIntersectionObserver from '@react-hook/intersection-observer'
const LazyIframe = ({ url, title }) => {
const containerRef = useRef()
const lockRef = useRef(false)
const { isIntersecting } = useIntersectionObserver(containerRef)
if (isIntersecting && !lockRef.current) {
lockRef.current = true
}
return (
<div ref={containerRef}>
{lockRef.current && (
<iframe
title={title}
src={url}
frameborder="0"
allow="accelerometer; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen="allowfullscreen"
></iframe>
)}
</div>
)
}
export default LazyIframe
The container div wrapping the iframe is used as a reference point, and that is tracked to see if the iframe has scrolled onto the page yet.
What About Cumulative Layout Shift?
Now we have a component which defers all scripts and video until the user scrolls onto the page. Great!
But as the user scrolls down the page we have a jump in content. A large YouTube video sized jank, as the previously empty space is filled by an iframe.
To solve this there needs to be a placeholder that can hold the shape of the video until it has loaded fully. Time for some trusty CSS.
We know that the container div will always be on the page, so we can use this as the placeholder. Then we fill that space with video once it has loaded.
The Final Solution
import { useRef } from 'react'
import useIntersectionObserver from '@react-hook/intersection-observer'
const LazyIframe = ({ url, title }) => {
const containerRef = useRef()
const lockRef = useRef(false)
const { isIntersecting } = useIntersectionObserver(containerRef)
if (isIntersecting) {
lockRef.current = true
}
return (
<div
style={{
overflow: 'hidden',
paddingTop: '56.25%',
position: 'relative',
width: '100%',
}}
ref={containerRef}
>
{lockRef.current && (
<iframe
title={title}
style={{
bottom: 0,
height: '100%',
left: 0,
position: 'absolute',
right: 0,
top: 0,
width: '100%',
}}
src={url}
frameborder="0"
allow="accelerometer; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen="allowfullscreen"
></iframe>
)}
</div>
)
}
export default LazyIframe
And there you are! A fully deferred iframe component to house YouTube videos. This could also be used for any oembed item.
Hopefully this helps! You can find me on Twitter if you have any questions.
Top comments (0)