Improving LCP for Video Hero Components

A hero component is an absolute staple in modern website designs. We have all seen plenty of them, a large media element at the top of the page, usually full-width, sometimes a-portion-of-the-screen tall, other times as tall as the entire initial screen.

And for the past several years, video hero components have been all the rage. Marketing loves them because we users love shiny things. You can even find collections of examples all over the Internet

Several examples of hero component layouts
Screenshot of video hero examples found on Dribbble.com

And I must admit, a nice splashy video at the top of the page, as the first thing you see, really does catch one’s attention! Except when it isn’t there…

TOC

The Problem

There are several challenges with using videos at the top of the page, including:

  • Videos typically take longer than images to download, because they tend to be larger files.
  • Videos cannot start playing until a certain amount has downloaded.
  • Videos cannot be preloaded, like images can be, so the browser doesn’t even know it needs to start downloading them until it sees the video element within the HTML.
  • Designers like to position promotional text over the video which can result in something like white text on a white background…

So until the browser finds the video element and downloads enough to start playing, there is nothing for the user to see. They just sit there, staring at that big old empty space, wondering whether something is happening, or whether something is broken…

And don’t even get me started on mobile users, using data connections, trying to download that big fat shiny video of yours…

Luckily, when this issue presents itself, it does so not only to the user, but also in our analytics! (You are using, and checking, your real-user monitoring (RUM), right?) Specifically, in this case, as a higher-than-normal Largest Contentful Paint (LCP) and, depending how the video player is added to the page, it could also be seen in a higher-than-normal Cumulative Layout Shift (CLS).

In this very light example page, the portrait view recorded an LCP of ~3.7s and the landscape ~3.4s; CLS recorded ~0.14 and ~0.07, respectively:

TOC ⇪

An Attempted Solution

Some of you keen observers may be saying “just use a poster, numbskull!” First of all, that is not nice. Second of all, posters do work well, except in two common situations:

  1. Responsive videos (when you need different aspect-ratio videos, depending on the screen size): The poster attribute/value can only be set on the video element, not on any source elements within the video element. This means you cannot have a different poster image for each different video aspect-ratio (i.e. portrait versus landscape layouts). In this case, users would always get one or the other poster image, regardless of their device’s screen size.
  2. Below-the-fold (not visible on the initial page load): If your video component is below-the-fold, you can lazy load the video asset (using preload="none" on the video element), but you can not lazy load the poster asset attached to that video element; it will download as soon as the browser sees it, which could delay another, more critical asset from downloading, and may even be completely unnecessary to download if the user doesn’t scroll down far enough to see it.

So, if you only have a single video asset that you want to serve for all device sizes, and it will always be above-the-fold, then a single video element with a poster attribute/value will work fine. Otherwise…

TOC ⇪

The Responsive Solution

If you have multiple video aspect-ratios, then you need multiple image aspect-ratio. And the easiest way to do that, is by using the picture element!

By creating a picture > source structure that matches the video > source structure, you can use CSS grid to layer them over one another. Specifically, layer the video over the picture.

To the user, they will see the image first (because it will download faster than the video), and when the video downloads enough to start playing, it will do so over the image. The effect is especially powerful if the image is the first frame of the video; in this way, the image sort of “comes to life”…

Using the same example page from above, but this time including a picture element, the portrait view recorded an LCP of ~1.9s (was ~3.7s) and the landscape ~2.0s (was ~3.4s); CLS recorded ~0.00 for both (was ~0.14 and ~0.07, respectively):

The HTML would be something like this:

<div class="container--media">
  <picture>
    <source 
            fetchpriority="high"
            width="720"
            height="1280"
            srcset="/path/to/portrait/image.webp" 
            media="(max-width: 480px)" />
    <source 
            fetchpriority="high"
            width="1280"
            height="720"
            srcset="/path/to/landscape/image.webp" 
            media="(min-width: 481px)" />
    <img src="" alt="Wonderfully descriptive text" />
  </picture>
  <video playsinline autoplay loop muted controls>
    <source 
            fetchpriority="high"
            width="720"
            height="1280"
            src="/path/to/portrait/video.mp4" 
            media="(max-width: 480px)" />
    <source 
            fetchpriority="high"
            width="1280"
            height="720"
            src="/path/to/landscape/video.mp4" 
            media="(min-width: 481px)" />
  </video>
</div>

The CSS needed to create the “video-over-picture” layout would be something like this:

.container--media {
  width: 100%;
  height: auto;
  display: grid;
  grid-template: "media";
  place-items: stretch;
  place-content: stretch;
  & > * {
    grid-area: media;
  }
  & img, video {
    display: block;
    width: 100%;
    height: auto;
    object-fit: cover;
  }
}

TOC ⇪

Comparison

Beyond the impressive metric improvements above, the visual side-by-side is quite telling:

TOC ⇪

Demo

Initially I created a CodePen, but stumbled on some inconsistencies that I assume are related to the iframe environment.

So, instead, both of the live demo pages from above are housed within a “lab” page on my server.

Notes:

  • Firefox does not autoplay for me, so you might need to manually click the play control. It loses some of the effect, but you can still see the image being “covered” by the video when the Image text disappears.
  • Again, as the demo pages are quite light, I highly recommending emulating a slower network connection to get the full effect (Fast or Slow 4G should suffice).

TOC ⇪

Optional Enhancements

Use aspect-ratio to prevent CLS

The example above that did not include the picture element experienced CLS because the container size was not known until the browser downloaded the video metadata.

If you wanted, you could use aspect-ratio to set the container size, but this will require syncing the breakpoints that you already have in your HTML with your CSS.

If you are okay with this, the additional CSS would look something like this:

...
& img, video {
  @media (max-width: 480px) {
    aspect-ratio: 720/1280 auto;
  }
  @media (min-width: 481px) {
    aspect-ratio: 1280/720 auto;
  }
}

Add "preload" links for each image

Depending on your page structure (and girth!) you may see little or no benefit from this, as was the case for my little demo pages; there simply isn’t enough other content being downloaded or activities happening to sufficiently delay the browser’s normally excellent work. But on a typical production, ecommerce site, I have seen this improve the LCP by 2-3s or more!

If you want/need to add "preload" links, the links would need to go into the head of the page, as high as possible, but after the meta name="viewport"; the HTML would look something like this:

  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="preload" as="image" fetchpriority="high" href="/path/to/portrait/image.webp" media="(max-width: 480px)">
  <link rel="preload" as="image" fetchpriority="high" href="/path/to/landscape/image.webp" media="(min-width: 481px)"> 

Content Overlay

Building on the above component, you can also overlay content, as in the demo pages.

Wrap the above HTML with something like this:

<div class="container--media-content">
  <div class="container--media">
    <picture>
      ...
    </picture>
    <video playsinline autoplay loop muted controls>
      ...
    </video>
  </div>
  <div class="container--content">
    <h1>My Brilliant Product</h1>
    <p>Something you really need to buy now!</p>
    <button>Add to Cart</button>
  </div>
</div>

The new CSS needed would be something like this:

.container--media-content {
  display: grid;
  grid-template: "media-content";
  place-items: center;
  place-content: start;
  & > * {
    grid-area: media-content;
  }
}
.container--content {
  position: relative;
}

Below-the-Fold

If the video component will be “below-the-fold” (initially offscreen, not visible to the user when the page first loads), then you will not want to use any preload link or fetchpriority attribute, and you will want to lazy load all of the image and video assets.

The resulting HTML would look something like this:

<div class="container--media">
  <picture>
    <source 
            loading="lazy"
            width="720"
            height="1280"
            srcset="/path/to/portrait/image.webp" 
            media="(max-width: 480px)" />
    <source 
            loading="lazy"
            width="1280"
            height="720"
            srcset="/path/to/landscape/image.webp" 
            media="(min-width: 481px)" />
    <img src="" alt="Wonderfully descriptive text" />
  </picture>
  <video playsinline data-autoplay loop muted controls>
    <source 
            preload="none"
            width="720"
            height="1280"
            src="/path/to/portrait/video.mp4" 
            media="(max-width: 480px)" />
    <source 
            preload="none"
            width="1280"
            height="720"
            src="/path/to/landscape/video.mp4" 
            media="(min-width: 481px)" />
  </video>
</div>

TOC ⇪

Challenges

For the most part, I feel like this is a pretty simple, elegant component, considering how versatile and robust it is. But, of course, nothing is perfect…

  • As I mentioned above, this layout is most effective when the image for each video is the first video frame. This isn’t hard to do, but it isn’t easy either, especially at scale. At one point, I began looking into how to “scrape” the first frame when the video is uploaded to a CMS, but the CMS my work was using at the time wasn’t going to allow this, so I moved on. It could still be an elegant solution…
  • But then you would also need a way of “connecting” the video and the image in the CMS, to make sure that, as you add a video to a page, you also get the correct image for each version of each image… Naming conventions could help, but again, at scale, this could easily fall apart.
  • Beyond syncing the correct image with the correct video, there is also a lot of syncing happening within the HTML, which might not always be in the same file/template:
    • If you are using preload links, the URLs for the images you add to each page need to be added to the preload links, which need to go into the head element.
    • If you are using preload links, the breakpoints for the images and videos within the content need to be synced with the breakpoints in the preload links. Ideally, this is a one-time, set-it-and-forget-it template update, but different layouts across different pages/templates could require different breakpoints.
    • The image and video assets need to be the exact same width and height, and those values have to be included in the source elements of both the picture and video elements.
    • If your page components can either migrate up and down the page, or be used anywhere within the page, considerations need to be made to deal with the correct autoplay, lazy load (and maybe preload link) settings.
    • If a video should autoplay, but it appears below the fold, know that currently there is no native way for a video to be lazy loaded and autoplay when it scrolls into view; to do this, you would need to convert the autoplay to something like a data-autoplay, then have JS look for that, using an IntersectionObserver to monitor when it gets close to the viewport, and trigger the autoplay programmatically.

Some of the above are also issues outside of this component, but I wanted to make sure they were stated.

TOC ⇪

Future Hope?

There is hope on the horizon for some of those challenges being discussed by some really smart people.

With luck, all of this could be solved some day by an adaptation to a standard.

TOC ⇪

Resources

I doubt very much that I am the first to think about this, but I am documenting my process nonetheless.

I was heavily influenced by the following:

Happy videoing,
Atg

2 Responses to Improving LCP for Video Hero Components

  1. Luke says:

    Really interesting and taught me a few things (and also might need to revert some recent recommendations – always learning!)

    On a general note, thank you for writing this up as it’s quite a common component and I’ve not seen a lot of guidance of this nature so it’ll come in handy for me and many others.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

To respond on your own website, enter the URL of your response which should contain a link to this post's permalink URL. Your response will then appear (possibly after moderation) on this page. Want to update or remove your response? Update or delete your post and re-enter your post's URL again. (Find out more about Webmentions.)