How to create web-optimized video with ffmpeg and the VP9 and H265 codecs

Every year, modern web development raises the bar on performance. Frameworks compete by optimizing network requests, images, and code to achieve blazing-fast loads. For images, almost any framework, headless CMS, or CDN offers built-in optimization options. Unfortunately, the same can't be said for video.
Still, there's a solution. Modern video codecs deliver excellent quality at a very reasonable size. A well-optimized video is always a better choice for animations than a GIF, and sometimes it even outperforms native implementations. In this article you'll see what the VP9 and H265 codecs are and how to optimize video with the ffmpeg tool.
Formats and codecs are two different things
This is a topic that lends itself to confusion, so it's worth getting the difference clear. Think of the video format as a container that defines the structure of the file carrying the video and audio tracks. Those tracks, inside the container, are media data encoded with a codec. Some popular video formats are mp4, WebM, or MKV. Among the best-known codecs are H264, H265, and VP8/9.
With that clarified, the next thing to know is that not every codec works with every format. WebM uses VP8 and VP9, while mp4 can carry almost anything.
For example, the video above, from Apple's new MacBook Air website, uses the mp4 format, but if you look closer you'll see it uses the H264 codec. H264 is very popular, but it's much less optimized than VP9 or H265.
WebM and VP9
WebM is an open, royalty-free media file format designed for the web. It can hold video tracks compressed with the VP8 or VP9 codecs and audio tracks compressed with the Vorbis or Opus codecs.
It lets you save between 20 and 50% in size compared to H264 while keeping the same quality level. It's supported by all modern desktop and mobile browsers, except Safari on iOS. Apple added WebM to desktop Safari starting with macOS Big Sur, but so far hasn't made any progress on the mobile version.
But don't worry: there's a solid alternative for iOS users.
MP4 and H265
H265, also known as HEVC (High-Efficiency Video Coding), is a codec supported by Apple devices that offers that same 20-50% efficiency improvement over its older sibling, H264. It isn't royalty-free, which may be why its adoption is quite a bit lower than WebM's.
So, by smartly combining webm + VP9 and mp4 + H265 for your videos, you can reach global coverage of 94%.
Tools for optimizing video
The two most popular tools for optimizing video are Handbrake and ffmpeg. Handbrake has a graphical interface, which makes it much easier to use; however, ffmpeg is more flexible if you want to render videos with an alpha channel (transparency) or use hardware acceleration, which speeds up compression enormously. Also, I personally find it much easier to share a single ffmpeg command with the team than to explain where to click in Handbrake.
Installing ffmpeg
If you're on macOS, install it with brew.
brew install ffmpeg
For other systems, follow the instructions on the official website.
ffmpeg basics
ffmpeg is a command-line tool with a huge number of options worth knowing. If you're just starting out, it's enough to run:
ffmpeg -i input.mp4 output.webm
The result will be a WebM file encoded with VP9 and decent compression out of the box. If you want to master the tool, you'll need to learn how to use its flags.
To make this clearer, I'll show you how each ffmpeg option reduces file size and affects quality. All the tests were done with an animation created for a recent project.
The original video weighs about 26 MB and has a resolution of 5000x2700.
But with basic compression it already comes down to 1 MB. Not bad, but it can be improved! 🚀
There are a few ffmpeg options that apply whether you use H265 or VP9:
-an: removes the audio track. Even if the file has no sound, it may include information for an empty audio track that adds weight, so always use it on videos without sound.
-vf: the option for passing filters to ffmpeg. The most common use case is scaling.
ffmpeg -i input.mp4 -vf scale=3840:-2 -an output.webm
As mentioned, the original resolution is 5000x2700, but it isn't displayed at that size, so you can reduce it to whatever the page actually needs. In this case, 3840 px wide. The scaling option keeps the original video's aspect ratio if you pass the width and an extra parameter with possible values -1, -2, or -n. The value -2 guarantees the resulting resolution is even. To learn more, take a look at this great demo here.
If you run the command above, the video drops to 542 KB.
Advanced settings
In addition to the arguments above, ffmpeg's VP9 and H265 codecs accept the -crf flag. It turns on constant quality mode, which guarantees a given perceptual quality level throughout the video. It takes a number from 0 to 63 (VP9) or from 0 to 51 (H265), where 0 is the best quality and the maximum value is the worst.
Any value between 18 and 40 can give quite decent quality. If you omit this option, ffmpeg defaults to -crf 32 for VP9.
ffmpeg -i input.mp4 -crf 40 -vf scale=3840:-2 -an output.webm
Result: 314 KB.
We're getting closer to codec-specific options, but first let's see how to explicitly tell ffmpeg which codec to use, instead of leaving it on automatic. That's what the -c:v flag is for. Some interesting options:
libvpx-vp9: the default when converting to WebM with the VP9 codec
libx265: for the H265 codec and the mp4 container
hevc_videotoolbox: for H265 with hardware acceleration
Example:
ffmpeg -i input.mp4 -c:v libvpx-vp9 -crf 40 -vf scale=3840:-2 -an output.webm
VP9 specifics
libvpx-vp9 has the -deadline option, which accepts the following values:
goodis the default and recommended value for most cases.bestis recommended if you have plenty of time and want maximum compression efficiency.realtimeis recommended for live or fast encoding.
Using best can save you a few extra KB.
ffmpeg -i input.mp4 -crf 40 -deadline best -vf scale=3840:-2 -an output.webm
Result: 295 KB total.
Time to move on to H265 and libx265. It's a very efficient library, but unfortunately it doesn't use hardware acceleration, so encoding takes quite a while. For H265 you need to know a few more options.
-preset is similar to deadline in VP9, but accepts many more values:
- ultrafast
- superfast
- veryfast
- faster
- fast
- medium (default)
- slow
- slower
- veryslow
- placebo
placebo is the slowest but, according to official tests, barely improves encoding. That's why it makes more sense to use veryslow.
Another flag you can pass is -movflags faststart. It optimizes the video for the web and makes it ready to play sooner. We haven't used it with VP9 in WebM because its architecture doesn't need it and always guarantees progressive loading.
And finally, -tag:v hvc1, which makes the video work on iOS and macOS devices. The command ends up like this:
ffmpeg -i input.mp4 -movflags faststart -c:v libx265 -vf scale=3840:-2 -crf 32 -preset veryslow -tag:v hvc1 -an output.mp4
And that gets us down to 260 KB!
You've probably noticed that for H265 we used -crf 32, while for VP9 it was -crf 40. Both values give a very similar bitrate in the two videos; the difference is just syntax, because the accepted number range is different.
Pros and cons of the hevc_videotoolbox library for H265
hevc_videotoolbox is a macOS-exclusive library that offers native hardware acceleration for encoding H265. It's two orders of magnitude faster than libx265, but it has its downsides: it optimizes worse and doesn't have the constant quality mode (-crf) or the presets from the previous example. As an alternative to -crf you can use -b:v, which lets you manually set the video bitrate (more information).
ffmpeg -i input.mp4 -movflags faststart -c:v hevc_videotoolbox -vf scale=3840:-2 -an -tag:v hvc1 output.mp4
Result: 654 KB.
The only advantage of hevc_videotoolbox over libx265 is that it lets you encode H265 videos with an alpha channel.
Sharpening the video
For some videos you can try lowering the resolution, but on retina screens the image may look blurry. Try applying the unsharp filter as shown below to improve perceived quality while keeping a low resolution:
ffmpeg -i input.mp4 -crf 40 -deadline best -vf scale=1920:-2,unsharp=5:5:1.0 -an output.webm -vf
For example, 5:5:1.0 gives a medium sharpening; for a soft one, try 3:3:1.0, and for a strong one, use 9:9:1.0. You can check this documentation for more details.
Perceived quality in moving images is very different from that of still images, so tricks like this help the video look good even if you shrink its size further.
With sharpening
Without sharpening
Alternatives to constant quality mode
ffmpeg offers more video optimization settings, such as Constrained Quality, two-pass encoding, constant bitrate, and Lossless. But they either drag out encoding time considerably, or require more trial and error to land on the right bitrate to get results similar to what you get with constant quality mode (crf).
Summary
Optimizing video for the web with ffmpeg is fairly straightforward in simple cases, and just two commands will do:
WebM (VP9)
ffmpeg -i input.mp4 -c:v libvpx-vp9 -crf 40 -vf scale=3840:-2 -deadline best -an output.webm
Command flag breakdown:
-c:v libvpx-vp9sets which codec to use-crf 40sets the constant quality mode settings-vf scale=3840:-2sets the scaling filter settings-deadline bestadjusts the compression speed/quality trade-off-anremoves the audio
MP4 (H265)
ffmpeg -i input.mp4 -c:v libx265 -crf 32 -vf scale=3840:-2 -preset veryslow -tag:v hvc1 -movflags faststart -an output.mp4
Command flag breakdown:
-c:v libx265sets which codec to use-crf 32sets the constant quality mode settings-vf scale=3840:-2sets the scaling filter settings-preset veryslowadjusts the compression speed/quality trade-off-tag:v hvc1makes the video work on iOS and macOS devices-movflags faststartoptimizes the video for the web-anremoves the audio
Tweaking the
-crfvalues will help you find the best option at the smallest possible size.
And to embed it in the project for the best possible result, use this HTML:
<video> <source type="video/webm" src="output.webm" /> <source type="video/mp4" src="output.mp4" /></video>
Troubleshooting
- If the video comes out with a green screen when converting to H.265, try adding the
-pix_fmt yuv420pflag - If you want to check video quality with different settings without waiting for the whole thing to encode, use the
-frames:v 1option, which renders only the given number of frames from the start of the video - If you're going to use the video for a frame-by-frame scroll animation, use the
-g 1flag. It will make the jump between frames smoother.