Animated WebP vs MP4 for Exercise Animations
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:
- From a transparent clip you can produce anything — composite any background, export MP4, resize per platform, re-theme next quarter. One build command, at the bottom of this post.
- From an opaque clip you cannot get the background back. Resize and re-encode still work, but the panel colour is in every pixel: no new theme, no round container, no clean transparency without re-rendering the exercise.
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:
| Clip | WebP 960, transparent | Same, composited (opaque) | MP4 crf 23 | MP4 crf 28 |
|---|---|---|---|---|
| Single Leg Glute Bridge | 1232 KB | 474 KB | 71 KB | 43 KB |
| Mountain Climbers | 1316 KB | 522 KB | 100 KB | 61 KB |
| Side-Lying Lateral Raise | 2186 KB | 1049 KB | 94 KB | 51 KB |
| Incline Dumbbell Curl | 3387 KB | 1381 KB | 128 KB | 65 KB |
| One Arm Kettlebell Push Press | 4093 KB | 1435 KB | 152 KB | 86 KB |
| Battle Ropes | 5022 KB | 1841 KB | 419 KB | 214 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.
What transparency buys a product
Not aesthetics — options you would otherwise have to buy again:
- Themes as a feature. If users pick or purchase themes, every exercise must look right on all of them. A transparent figure composites onto whatever the theme sets; an opaque one needs an export per theme, from whoever owns the artwork.
- White-label. The same catalogue in a gym chain’s palette, without re-cutting a file.
- Dark mode, which is the same problem and catches most teams after launch.
- Any container — round avatars, rounded cards, overlays on a photo or gradient. No white box, no fringe.
- MP4 on demand. Pick the background, run the recipe below, ship video where it helps, regenerate it at the next redesign.
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:
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 12 | Bytes | Load | Sustained frame rate |
|---|---|---|---|
| 480px stills | 0.26 MB | 54 ms | 56 fps |
| 960px animated WebP | 37.92 MB | 83 ms | 56 fps |
| MP4, background baked in | 1.97 MB | 296 ms | 28 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:
| Clip | Frames | 960 px | 480 px |
|---|---|---|---|
| Mountain Climbers | 34 | 4.48 ms/frame | 0.82 ms/frame |
| Battle Ropes | 99 | 4.61 ms/frame | 0.95 ms/frame |
| Incline Dumbbell Curl | 60 | 5.16 ms/frame | 1.09 ms/frame |
| Side-Lying Lateral Raise | 41 | 5.41 ms/frame | 1.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:
- MP4 is far cheaper to download, rides the video pipeline, and gets expensive in bulk — visibly, in the page’s frame rate.
- Animated WebP is heavier to download, costs ~10% of a core per master clip to play, and degrades invisibly.
- A still beats both wherever nobody is studying technique.
Which is a good reason to distrust the dashboard here and watch the animation on a real device.
Choosing between them
| Requirement | Choice |
|---|---|
| Asset must sit on light, dark, and brand surfaces | Animated WebP |
| One fixed background across the product | MP4 derivative |
Display with a plain <img> | Animated WebP |
| Play, pause, seek, visibility control | MP4 |
| Several clips visible at once | Neither — stills, one active clip |
| Bandwidth-critical, single active clip | MP4 |
| A master you can re-theme without a transcode step | Animated 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:
- catalogue and search — still WebP;
- exercise detail — transparent animated WebP over the current theme;
- branded workout player — MP4 with the player background baked in;
- full screen — the 960px asset, loaded after the user opens it.
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:
- Frame rate. The colour source dictates the output rate through the overlay, so both
-framerateandr=must match the clip. Assume 25 and you ship duplicated frames; thewebpinfoline above reads the real rate. - Alpha edges. Check the result on both a light and a dark test background. Incorrect compositing shows up as a pale fringe around thin equipment and soft edges.
- crf. 23 is a safe default; 28 roughly halved the files again with no obvious loss at card size — illustration artwork on a flat background compresses well.
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:
- compressed size, and total bytes for a full screen of cards;
- time to first useful frame;
- sustained frame rate while the screen is visible;
- memory after opening and closing several exercises;
- behaviour on the slowest device you support, on mobile data.
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.