D
P
0
← All articles Baca dalam Bahasa Indonesia

CSS Layout, Overflow & Cascade

`out.webm` Came Out Bigger Than `in.mp4`? VP9 CRF Is a Quality Target, Not a Size Target

· · 5 min read
`out.webm` Came Out Bigger Than `in.mp4`? VP9 CRF Is a Quality Target, Not a Size Target

I carried one assumption for far too long: if the new format is more efficient, the output must be smaller. VP9 really is more efficient than H.264. But codec efficiency promises nothing about file size unless you tell the encoder how generous it is allowed to be.

I learned that while cleaning up the media pipeline on my own portfolio site. The project demo videos were still stored as mp4, and I wanted them as VP9 webm so the project pages would load lighter. One command, one pass, done. That was the plan.

The symptom: the webm outweighed its own mp4 source

I used the number that shows up in almost every VP9 for web recommendation:

ffmpeg -y -i in.mp4 -an \
  -c:v libvpx-vp9 -crf 33 -b:v 0 \
  -row-mt 1 -cpu-used 4 -pix_fmt yuv420p \
  out.webm

The encode succeeded. No suspicious warnings. But when I lined the input and output sizes up side by side, several webm files were larger than the mp4 they came from. Not by a rounding error either, but by enough that the entire reason for doing the conversion had evaporated.

My first instinct pointed the wrong way. I suspected -b:v 0 was letting the bitrate run wild, then suspected -cpu-used 4 was trading compression for speed. Neither was the culprit.

The root cause: CRF answers a different question

In libvpx-vp9, -crf is a constant quality target, not a size target. Paired with -b:v 0 you are explicitly removing the bitrate ceiling and telling the encoder: hit this quality level, spend whatever you need.

That is where it fell apart. My source videos had already been compressed hard in an earlier step. The fine detail was long gone, and part of what remained was compression artifacts: blocking, banding, and synthetic noise that is genuinely hard for an encoder to predict.

VP9 has no way of knowing which pixels are real detail and which are scar tissue from a previous encode. To it, all of that is frame content that must be reproduced faithfully until the crf 33 target is met. So the encoder worked hard, and in doing so spent more bits than the source itself had used.

crf 33 was never the wrong number. It was simply far too generous for a source with that little quality left. My actual mistake was treating CRF as one global constant when it has to be set relative to the quality of each source.

The fix: two numbers instead of one

Once the framing was right, the fix got boring. I split the videos into two groups.

New high quality clips that had never been re-compressed kept crf 33:

ffmpeg -y -i in.mp4 -an \
  -c:v libvpx-vp9 -crf 33 -b:v 0 \
  -row-mt 1 -cpu-used 4 -pix_fmt yuv420p \
  out.webm

The older, already compressed videos moved up to 38:

ffmpeg -y -i in.mp4 -an \
  -c:v libvpx-vp9 -crf 38 -b:v 0 \
  -row-mt 1 -cpu-used 4 -pix_fmt yuv420p \
  out.webm

The numbers immediately started making sense. The existing set went from 107 MB down to 76 MB, with every single file finally smaller than its source. The new clips, previously 17 to 20 MB each, dropped to roughly 2 MB at crf 33, because those genuinely had surplus quality worth throwing away.

Note that exactly one value changed. Everything else stayed put:

Thumbnails while I was in there

Since I was already reorganizing media, the static thumbnails went to WebP with a sensible width cap:

ffmpeg -i in.png -vf "scale='min(1600,iw)':-2" -c:v libwebp -q:v 82 out.webp

min(1600,iw) prevents small images from being upscaled, and -2 keeps the resulting height even so the encoder does not complain.

A quick check before committing

The uncomfortable lesson: I never compared input and output sizes until the whole batch was already converted. Now that comparison is part of the process rather than a surprise at the end:

for f in *.mp4; do
  printf "%s  %s -> %s\n" "$f" \
    "$(du -h "$f" | cut -f1)" \
    "$(du -h "${f%.mp4}.webm" | cut -f1)"
done

If any row points at a bigger number, raise the CRF for that file and re-encode. No clever tooling needed, just one honest table.

What I took away