RepDB
← All posts

Animated WebP vs MP4 for Exercise Animations

August 12, 2026 · 12 min read · by Sergei Argutin

One transparent master, two deliveries: when to ship the animated WebP and when to export an MP4 — measured on real exercise clips.

Animated WebP or MP4? Most write-ups end with “it depends, measure it”, so I measured it on the clips RepDB ships. The useful answer is not a winner, it is an order of operations:

So the transparent WebP is the master and MP4 is a delivery format you generate from it. Below: what each costs, and what it locks in.

The measured comparison

Six clips, spanning the size range of the catalogue. Every variant re-encoded from the same source with the settings the bundle ships with (quality 90, method 4, exact alpha); MP4 is H.264 crf 23, medium preset, background composited to #111827:

ClipWebP 960, transparentSame, composited (opaque)MP4 crf 23MP4 crf 28
Single Leg Glute Bridge1232 KB474 KB71 KB43 KB
Mountain Climbers1316 KB522 KB100 KB61 KB
Side-Lying Lateral Raise2186 KB1049 KB94 KB51 KB
Incline Dumbbell Curl3387 KB1381 KB128 KB65 KB
One Arm Kettlebell Push Press4093 KB1435 KB152 KB86 KB
Battle Ropes5022 KB1841 KB419 KB214 KB

Read it left to right: one master and two things you can make from it, not three products to choose between.

Fixing the background saves ~2.5×. Same format, same settings, alpha gone — the median file drops to 39%. That is what the alpha channel costs.

Encoding as video saves another ~8×, because a video codec has interframe prediction and an animated image format does not. End to end: 3387 KB → 128 KB for Incline Dumbbell Curl, from a file you already have.

The master is the biggest file in the table. That is what a master is for. The question is not which one you want, it is which one goes on this screen — and you can answer it per screen only because you hold the transparent version.

Dark mode UI
no white square, no rework
Light card
drops cleanly on cream
Brand gradient
drops on any UI surface
Round avatar
circular mask, no white fringe

What transparency buys a product

Not aesthetics — options you would otherwise have to buy again:

Choosing a transparent master is not choosing against MP4; it keeps MP4 as something you can produce for any background, at roughly 5% of the bytes (median of the six clips above). Same movement, both ways, with the surface on a toggle:

Surface:
Mountain Climbers — transparent animated WebP
Transparent WebP
559 KB · composites over anything
MP4, background baked in
32 KB · exported for one surface
Flip the surface. The transparent clip follows it; the video brings its own panel colour along, which is correct on dark and plainly wrong on cream — for 32 KB instead of 559 KB.

The video is a fraction of the bytes and completely correct — on the panel it was exported for. Flip the surface and it is simply wrong. Fine, as long as it is a derivative you can regenerate rather than the only copy you own.

One clip of each is a load any machine handles. Put several animated images on a page and the image path is the one that slips first, for a measurable reason.

What MP4 costs you back

A twelve-card catalogue grid, three ways, every tile at 240 CSS px, in Chrome on a desktop over localhost:

Grid of 12BytesLoadSustained frame rate
480px stills0.26 MB54 ms56 fps
960px animated WebP37.92 MB83 ms56 fps
MP4, background baked in1.97 MB296 ms28 fps

The MP4 grid moved 20× fewer bytes and ran at half the frame rate: twelve <video> elements decoding and compositing at once is real work, and each carries playback machinery an image does not. Pausing eleven put it back to 56 fps; resuming them dropped it to 28 again.

The WebP row needs a warning label

That 56 fps is the page’s frame rate from requestAnimationFrame. An animated image decodes and rasters outside the page’s animation loop, so a clip can drop half its frames while the page reports a calm 56 — and no browser API exposes an image’s real playback rate (drawing it to a canvas returns a frozen frame).

The decode work can be measured, with the browser’s ImageDecoder. Four clips, every frame, best of three passes:

ClipFrames960 px480 px
Mountain Climbers344.48 ms/frame0.82 ms/frame
Battle Ropes994.61 ms/frame0.95 ms/frame
Incline Dumbbell Curl605.16 ms/frame1.09 ms/frame
Side-Lying Lateral Raise415.41 ms/frame1.07 ms/frame

At the clips’ own 20 fps that is 90–108 ms of decode per second of playback for a 960px clip — a tenth of a core, per clip, on a fast desktop. At 480px, 16–22 ms.

Twelve of them need ~1.1 s of decode per second of wall clock. Nothing plays correctly under that. The page stayed at 56 fps because the animations, not the page, absorbed the shortfall — where the metric could not see it.

So:

Which is a good reason to distrust the dashboard here and watch the animation on a real device.

Choosing between them

RequirementChoice
Asset must sit on light, dark, and brand surfacesAnimated WebP
One fixed background across the productMP4 derivative
Display with a plain <img>Animated WebP
Play, pause, seek, visibility controlMP4
Several clips visible at onceNeither — stills, one active clip
Bandwidth-critical, single active clipMP4
A master you can re-theme without a transcode stepAnimated WebP

RepDB ships the transparent WebP as the source of truth: images/animations/{slug}.webp, 960×960, ≤20 fps, one file per movement — resolve the slug through image_alias when a record has one, since close variants reuse the base movement’s clip. The animation library shows those files on different surfaces — the flexibility you give up by baking in a background.

The two-variant strategy

For most products it is not one format forever:

The transparent clip stays the master; the MP4 is a derivative for one known context. Regenerating it later is a build step, not a re-shoot.

Converting: the recipe that actually works

This is where the standard advice fails. The obvious command —

# does NOT work on these clips
ffmpeg -i images/animations/mountain-climbers.webp ... output.mp4

— fails on FFmpeg 8.1 with image data not found. FFmpeg only learned to decode animated WebP in 7.1, and its decoder still chokes on clips whose frames are stored as blended sub-rectangles rather than full canvases, which is what a size-optimised encoder produces. Every clip I tried failed the same way.

Extract the frames with libwebp’s anim_dump first — it composites each frame onto the full canvas — then encode the PNG sequence:

CLIP=images/animations/mountain-climbers.webp

# 1. frames out (libwebp ships anim_dump alongside cwebp)
mkdir -p frames
anim_dump -folder frames "$CLIP"

# 2. the clip's own rate rather than a guess: frames / total duration
FPS=$(webpinfo -summary "$CLIP" \
  | awk '/Duration/ {n++; ms += $2} END {printf "%.0f", n / (ms / 1000)}')

# 3. composite on the panel colour and encode
ffmpeg -y \
  -framerate "$FPS" -i "frames/dump_%04d.png" \
  -f lavfi -i "color=c=#111827:s=960x960:r=$FPS" \
  -filter_complex "[1:v][0:v]overlay=shortest=1:format=auto,format=yuv420p[v]" \
  -map "[v]" -an \
  -c:v libx264 -crf 23 -preset medium -movflags +faststart \
  mountain-climbers.mp4

Three things that bite:

Do not use MP4 as a transparent replacement

If users can switch themes, or the asset appears on more than one surface, MP4 is not a drop-in substitute: one export per background, and no runtime compositing. The 20× is real and so is that constraint.

Equally, do not reach for the 960px clip just because it exists. In a 240px card the answer is usually a 46 KB still — How to Use Exercise Images in a Fitness App has the per-screen breakdown.

How to benchmark your own choice

Compare clips at the same displayed size and perceived quality, and measure the page rather than the file:

The gap between formats is usually smaller than the gap between twelve things playing and one. Fix that first — lazy-loading and the still-first pattern covers how.

Where these clips come from

Everything measured here is RepDB’s shipped artwork: looping exercise animations at 960px on transparent backgrounds, which is what made both columns of that table come from one source. The opaque and MP4 variants were an export, not a re-render — and that is only possible because the master keeps its alpha channel.

FAQ

Is MP4 always smaller than animated WebP?

On this content, by a wide margin: a median of 5% of the transparent WebP’s bytes, 3.7–8.3% across six clips. Two-thirds of that is video compression, one-third is dropping the alpha channel.

Can a normal MP4 keep a transparent background?

Not in any broadly compatible way. Standard H.264 delivery is opaque, so you composite a background before encoding — that is the trade.

Should a fitness app use MP4 for every exercise?

No. Stills for catalogue views, transparent WebP where the background varies, MP4 where the background is fixed and one clip plays at a time.

Why keep the transparent WebP if I export MP4 anyway?

It is the theme-independent master. The MP4 is a derivative for one UI context, and you regenerate it whenever that context changes.

What should I test first?

Your busiest screen, with a clip from the middle of the size range — around 3 MB for this catalogue. A short, simple loop will flatter whichever format you are testing and tell you nothing about the screen that actually hurts.

Method

Six clips from the current build, 1.3–5.1 MB, covering the size range of the 442 shipped animations. WebP variants re-encoded from the originals at quality 90, method 4, exact=True, disposal 2 — the settings the bundle ships with. MP4 encoded with FFmpeg 8.1, libx264, crf 23/28, medium preset, composited on #111827. Grid test: Chrome on a desktop machine, twelve cards at 240 CSS px in a four-column grid, served over localhost, frame rate sampled over three seconds via requestAnimationFrame. Localhost removes the network entirely, which is why the byte column deserves more weight than the load column.

Decode cost: Chrome’s ImageDecoder, decoding every frame of each clip, best of three passes, with the file already in memory — so it isolates decode from network and layout. “Per second of playback” multiplies the per-frame cost by the clips’ own 20 fps. Reading an animated image’s actual on-screen frame rate is not possible from page JavaScript: drawImage into a canvas returns a static frame, and there is no equivalent of requestVideoFrameCallback for images. Where this post says an animation stutters, that is an observation, not a metric — the decode budget is the measurement that explains it.

Want the actual files to benchmark against? The preview viewer runs the production assets in the browser; the full catalogue and licence terms are on the pricing page.