You might have come across some of the videos on our website and thought: “Gee wowsers, they’re still hitting near perfect performance scores in Lighthouse”. Well, let me tell you: it was a struggle.
However, the good news is, today we want to share what took us far too long to perfect, and give you the opportunity to use Mux yourself, without tanking performance with the Mux Player.
It's also worth mentioning that the Mux player has more compatibility and better video experience (see Feedback from Dylan Jhaveri for more context) but frankly, I'm fairly certain folks aren't browsing our website on IE8, nor do we want to build for IE8, so this is the compromise we are willing to make (sorry dinosaurs 🦕).
The code
'use client';
// Swap these out for another icon set if you want
import { Loader2, PlayIcon } from 'lucide-react';
// To dynamically load the video player
import dynamic from 'next/dynamic';
import { FC, useState } from 'react';
import { preload } from 'react-dom';
// This is from Sanity type generation
import { MuxVideo, Video as VideoType } from '~/sanity.types';
const getVideoToPlayUrl = (muxId: string) => {
return `https://stream.mux.com/${muxId}.m3u8`;
};
const getPosterUrl = (muxId: string) => {
return `https://image.mux.com/${muxId}/thumbnail.webp?fit_mode=smartcrop&time=0`;
};
const getMuxVideoProps = (muxId: string) => {
return {
poster: getPosterUrl(muxId),
vidUrl: getVideoToPlayUrl(muxId),
};
};
type VideoPlay = {
poster: string;
vidUrl: string;
loop?: boolean;
muted?: boolean;
control?: boolean;
};
const VideoAutoPlay = ({ control, loop, muted, poster, vidUrl }: VideoPlay) => {
const HlsVideo = dynamic(
() => import('./hls-video').then((mod) => mod.HlsVideo),
{
loading: () => {
return (
<div className="relative size-full">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={poster}
// Pick your most commonly used video size to avoid CLS
width={1920}
height={1080}
alt="a"
className="mx-auto size-full "
/>
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 transform ">
<Loader2 className="animate-spin" size={112} strokeWidth={0.5} />
</div>
</div>
);
},
},
);
return (
<HlsVideo
autoPlay
poster={poster}
vidUrl={vidUrl}
loop={control ? loop : undefined}
muted={control ? muted : undefined}
controls={control}
width={1920}
height={1080}
className="mx-auto size-full"
/>
);
};
const VideoWithOutAutoPlay = (
props: VideoPlay & { loading?: 'lazy' | 'eager' },
) => {
const { poster, loading } = props;
const [play, setPlay] = useState(false);
return (
<div className=" size-full">
{play ? (
<div className="relative size-full ">
<div className="my-4">
<VideoAutoPlay {...props} />
</div>
</div>
) : (
<div className="relative size-full">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={poster}
width={1920}
height={1080}
loading={loading === 'lazy' ? 'lazy' : 'eager'}
alt="a"
className="mx-auto size-full "
/>
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 transform ">
<button
className="grid place-items-center rounded-full bg-black p-8 hover:bg-hotpink-400"
onClick={() => setPlay(true)}
>
<PlayIcon className="h-8 w-8 fill-white text-white" />
<span className="sr-only">Play</span>
</button>
</div>
</div>
)}
</div>
);
};
export type VideoProps = VideoType & {
videoEmbed: NonNullable<VideoType['videoEmbed']> & {
video: MuxVideo & {
asset: {
playbackId: string;
};
};
};
};
export const Video: FC<VideoProps & { loading?: 'lazy' | 'eager' }> = ({
videoEmbed,
loading = 'eager',
}) => {
const { video, autoPlay, control, loop, muted } = videoEmbed ?? {};
const { poster, vidUrl } = getMuxVideoProps(video?.asset?.playbackId);
preload(vidUrl, {
as: 'video',
fetchPriority: 'high',
});
preload(poster, {
as: 'image',
fetchPriority: 'high',
});
const videoProps = { poster, loading, vidUrl, loop, muted, control };
return (
<div className="mx-auto flex max-w-7xl">
{control && autoPlay ? (
<VideoAutoPlay {...videoProps} />
) : (
<VideoWithOutAutoPlay {...videoProps} loading={loading} />
)}
</div>
);
};
Performance improvements
Well remember how there was some bloke called Charles Goodhart said: "When a measure becomes a target, it ceases to be a good measure". Well we basically ignored everything that person said, and decided to optimise to see if it was possible to hit 100/100 on a website that still has a lot of images, fancy libraries and analytics.
If you're looking to try and get the perfect score on your website, get in touch with us, because we sure as hell know a lot about this stuff, after all the hassle we've been through to try and achieve it.
A quick explainer what this is good for
Feedback from Dylan Jhaveri
We posted this on Vercel Community, and got some great feedback and better context. So, before you go ahead and straight up copy the code without knowing the tradeoffs make sure you read this bit.
"Love the work you’ve done in your example to optimize for lighthouse score. There is a fundamental tradeoff here that I think folks should think about:
web vitals (lighthouse score) vs. video performance
The biggest change that I can see, if I’m following correctly is waiting to load <hls-video>
until after the user clicks play.
That’s going to make your lighthouse score great, (which is great!) – but the tradeoff is that when the user clicks play it’s going to take more time for playback to start and the user to see the first frame of the video.
For your use case on roboto.studio I think that probably makes perfect sense. For other use cases where you want the video to load fast you may want to lazy load the player 2 (https://docs.mux.com/guides/player-lazy-loading), which tries to balance the tradeoffs:
- Doesn’t block the initial page load (despite this, your lighthouse scores will be impacted negatively)
- Lazy loads the player without requiring the user to click play so that when the user does click play the video starts up immediately"
Cheers Dylan, and thanks for the free content ✌️ go read his blog posts here