Replace animated GIFs with video
Why animated GIFs are so large, how Lighthouse flags them, how to turn one into MP4 and WebM with FFmpeg, and the video markup that acts like a GIF.
In this guide
Why a GIF is so large
GIF dates from 1987. MDN explains that it uses the Lempel-Ziv-Welch algorithm to compress 8-bit indexed color graphics without loss. Each pixel is a number that points into a palette. The web.dev Learn Images course spells out the price: a frame can hold at most 256 colors, so photos and gradients turn blotchy, and the lossless compression does not shrink a file much.
A GIF can store a later frame as just the part that changed, MDN notes. Video goes further. MDN's codec guide says video compression compares frames, finds where they differ and records only what is needed to update the previous frame. It is lossy, and it adds a full key frame now and then to clear away any build-up of error. The web.dev course draws the conclusion: animation is the most common modern use of GIF, and video formats serve it far more efficiently, and more accessibly.
What Lighthouse reports
Older versions of Lighthouse had an audit named "Use video formats for animated content". In Lighthouse 13.5 the Improve image delivery insight does that job, and its source lists the old audit among the ones it replaces. The rule, in code that Lighthouse shares with Chrome DevTools: a GIF is flagged when the file is larger than 100 KiB. It reads the file type and the size, so a large still GIF is flagged too.
The saving it shows comes from a formula. A comment in the source says the formula is based on 1000 real GIFs converted to video. Worked out from that formula, the estimate is:
| GIF file size | Estimated saving |
|---|---|
| 100 KiB | 45% |
| 500 KiB | 65% |
| 1 MiB | 74% |
| 5 MiB | 95% |
These are Lighthouse's rough estimates for a typical clip, not a measurement of yours. Convert your file and compare the real sizes.
Placement matters too. The first frame of an animated image can be the page's Largest Contentful Paint element, so a heavy GIF high on the page delays the whole page. See Largest Contentful Paint.
Convert the GIF to video
Lighthouse's documentation uses FFmpeg, a free command-line tool, and shows two commands. These are the same two, with one change that is explained below:
# MP4 (H.264): plays in nearly every browser
ffmpeg -i my-animation.gif -pix_fmt yuv420p my-animation.mp4
# WebM (VP9): usually smaller
ffmpeg -i my-animation.gif -c vp9 -b:v 0 -crf 41 my-animation.webmThe -i option names the input file, and -c vp9 picks the VP9 codec. The -crf 41 setting is the quality value from Lighthouse's example. In a test I ran, higher numbers gave smaller, rougher files. Try a few values and keep the smallest file that still looks right.
Lighthouse notes that WebM files are much smaller than MP4 files, and that browsers differed in WebM support when the page was written, so it suggests making both. MDN now calls WebM natively supported in most browsers and MP4 widely supported across devices and browsers. Offer both, and let each browser take the first one it can play.
Ask for the common color format
The change from Lighthouse's command is -pix_fmt yuv420p. The original MP4 command leaves the color format to FFmpeg. In a test with FFmpeg 6.1 on a generated GIF, that produced H.264 "High 4:4:4 Predictive", a profile few players are built around. MDN says most AVC profiles use 4:2:0 color and that the profiles differ in what they can do. Asking for yuv420p gets the common kind. FFmpeg's companion tool, ffprobe, shows what you got:
ffprobe -v error -select_streams v:0 -show_entries stream=codec_name,profile,pix_fmt -of csv=p=0 my-animation.mp4The answer you want is h264,High,yuv420p.
Even sizes, and the right size
H.264 with 4:2:0 color needs an even width and height. When I gave FFmpeg a GIF 481 by 271 pixels, it stopped with "width not divisible by 2". Resize in the same command. The scale filter's -2 keeps the picture's shape and, as FFmpeg's documentation says, rounds that side to a multiple of 2:
ffmpeg -i my-animation.gif -vf "scale=480:-2" -pix_fmt yuv420p my-animation.mp4Pick the width at which the clip is shown on the page, or a little more for sharp screens, not the width of the original. MDN's codec guide adds that a smaller frame size and a lower frame rate both make a file smaller. It says that below about 12 frames per second video reads as a series of still pictures, and that 24 and above looks smooth. The fps filter sets the rate, for example -vf "fps=15,scale=480:-2". Try a rate and look at the result.
If you would rather not use the command line
Many image CDNs turn an uploaded GIF into HTML5 video for you, Lighthouse says. Its advice for WordPress, Drupal and Joomla sites is to upload the GIF to a service that makes it available to embed as HTML5 video. WordPress's own Video block can also play an MP4 from the media library: the block definition lists the attributes autoplay, loop, muted, playsInline, poster and preload. After you publish, view the page source and check that the video tag carries all four of autoplay, loop, muted and playsinline.
Put the video on the page
Lighthouse names three things a GIF does that a video must copy: it plays by itself, it repeats, and it is silent. The video element does all three:
<video autoplay loop muted playsinline
width="480" height="270" poster="/img/demo-poster.jpg">
<source src="/video/demo.webm" type="video/webm; codecs=vp9">
<source src="/video/demo.mp4" type="video/mp4">
</video>| Attribute | What it does |
|---|---|
autoplay | Starts the video without a click. |
muted | Silences it. MDN lists muted media among the cases where autoplay is allowed. Browsers block autoplay for media with sound. |
loop | Repeats the clip, as a GIF does. |
playsinline | Asks the browser to play the video inside the page. It is part of Lighthouse's recipe. |
width, height | Reserve the space before the file arrives. MDN recommends them on every video to avoid layout shift. |
poster | An image to show while the video downloads. |
Put the smaller format first. MDN says the browser tries each source in order. When a source has a type, the browser compares it with the formats it can play and skips the ones it cannot without asking the server. A video with no controls shows no buttons, which suits a GIF-like loop. See below for the pause control.
Keep the poster small and the same shape as the video. For a clip at the top of the page, the poster or the first frame can be the LCP element. Set the size too: see image width and height.
Load it late when it is below the first screen
A video with autoplay starts downloading when the page does. For a clip far down the page that wastes data and competes with what the visitor sees first. MDN notes that autoplay takes precedence over preload. It also lists a loading attribute for video, marked experimental, so check its compatibility table before relying on it.
A dependable pattern leaves out autoplay, sets preload="none" with a poster, and starts the video with a few lines of script when it nears the screen. The script below also respects the visitor's reduced-motion setting:
<video muted loop playsinline preload="none"
width="480" height="270" poster="/img/demo-poster.jpg" data-autoplay>
<source src="/video/demo.webm" type="video/webm; codecs=vp9">
<source src="/video/demo.mp4" type="video/mp4">
</video>
<script>
const calm = matchMedia('(prefers-reduced-motion: reduce)').matches;
const watcher = new IntersectionObserver((entries) => {
for (const entry of entries) {
if (entry.isIntersecting) entry.target.play().catch(() => {});
else entry.target.pause();
}
}, { rootMargin: '200px' });
document.querySelectorAll('video[data-autoplay]').forEach((v) => {
if (calm) v.controls = true;
else watcher.observe(v);
});
</script>The observer reports when the video comes within 200 pixels of the screen. The play() method returns a promise that is rejected when the browser refuses playback, so the code catches that and the poster simply stays. I ran this page in headless Chromium: before scrolling, only the poster had been requested; after scrolling the video into view, it was requested and played; scrolling away paused it. Without JavaScript, the visitor sees the poster only. For a clip in the first screen, use plain autoplay. See also lazy loading.
Keep it accessible and comfortable
A GIF never stops, and neither does a looping video. WCAG success criterion 2.2.2, as MDN summarizes it, asks for a way to pause, stop or hide moving content that starts automatically, lasts longer than five seconds and appears next to other content, unless the movement is essential. A loop has no end, so give it a control. A real button that calls play() and pause() on the video is enough, and a keyboard can reach it.
Some visitors ask their device for less motion. MDN explains that the prefers-reduced-motion setting exists because some animations, such as scaling or panning large objects, can cause discomfort for people with vestibular motion disorders. The script above reads the setting with matchMedia. For those visitors the video does not start by itself, and the controls appear so they can choose to play it. The plain markup in the previous section starts for everyone, so use the script version when the animation is large or fast.
Movement can carry meaning. If the clip shows something the text does not say, such as the steps of a process, write the same information as text on the page. A GIF has no sound, but if you replace it with a clip that has some, MDN says to provide captions and a transcript.
When a video is not the right replacement
Some places take only an image, such as the image field of a tool you do not control. Choose the next best thing. MDN's table of image types says to consider WebP, AVIF or APNG instead of GIF for animation, and describes the older formats as performing poorly next to WebP and AVIF. Animated WebP and AVIF work in an img element, so nothing else on the page changes. Check MDN's compatibility table for animated AVIF, then convert to each format and compare the files. See image formats.
A small animated icon is not worth much effort. Lighthouse only flags GIFs over 100 KiB, so fix the biggest files first.
How to check
- Read the insight. In Lighthouse or PageSpeed Insights, open Improve image delivery. A flagged GIF carries the note "Using video formats instead of GIFs can improve the download size of animated content", with an estimated saving.
- List every GIF. In the Network panel, filter by
mime-type:image/gifand sort by size. See the Network panel. - Compare after the change. Reload with the panel open. The video requests should show a transfer size well below the GIF's.
- Check the file. Run the ffprobe command above on the MP4.
- Check the server. MDN warns that when the MIME type of a video is wrong, the video may not show, or may show a gray box with an X. Look at the
Content-Typeheader of each video request. - Try a real phone. The clip should start by itself. See testing on real phones.
Mistakes to avoid
- Leaving out `muted`. Browsers block autoplay for media with sound, so the clip may not start.
- Keeping the FFmpeg default color format. Check with ffprobe that the MP4 is
yuv420p. - No width and height. The page jumps when the video arrives.
- A large clip shown small. Make the file about the size it is shown.
- Autoplaying a clip far down the page. Start it when it nears the screen.
- No way to pause. A loop that never ends needs a control.
- A wrong MIME type on the server.
- Converting a tiny GIF first. Start with the largest files.
Check your own page
Run the free audit and look in the image findings for a GIF. Then open the Network panel, filter for mime-type:image/gif, and note the size of each file. Convert the largest one and compare.
Sources
- Image formats: GIF in the web.dev Learn Images course: how GIF compresses, the 256-color limit and why video suits animation.
- Use video formats for animated content in the Lighthouse documentation: the FFmpeg commands, the three traits of a GIF, the markup and image CDNs.
- ImageDelivery.ts in the Chrome DevTools frontend, and image-delivery-insight.js in Lighthouse 13.5: the 100 KiB rule, the estimate and the audit it replaces.
- MDN: the video element, the autoplay guide, the source element, the image types guide, the video codec guide, prefers-reduced-motion and Understanding WCAG: Operable.
- FFmpeg filters documentation: the
scaleandfpsfilters. - The video block definition in the Gutenberg repository.
Start here
Free website audit
Live Google data for any public page: mobile and desktop.
Mobile + desktop · Lighthouse lab test · Real-user Chrome data when Google has it
Rather have this done for you? Speed optimization · Book a call