DEV Community

hao jia
hao jia

Posted on

I replaced a docs-site GIF with animated WebP and it got bigger

I had a one-line plan for our internal docs site: replace every GIF with animated WebP. Twenty-odd components, one screen recording each, and that layer was eating close to half of the first-load traffic. WebP is a decade and a half newer than GIF, so the win felt too obvious to measure.

I measured it anyway. On my sample, animated WebP came out 6.4% larger than simply re-encoding the GIF.

The sample

I drew a fake back-office animation frame by frame with a script — a made-up order management page: skeleton shimmer, rows filling in, a filter dropdown opening, a confirmation dialog, and a clock ticking in the header. 960×600, 75 frames, 60 ms each, 4.5 seconds. As a GIF it weighs 1,585,112 bytes.

It is a synthetic file, not the output of a real screen recorder, so treat every number below as describing this kind of picture — a mostly static UI recording — and nothing broader.

Four formats, one export run

I dropped it into the animation workshop on ImgIng (https://imging.ai/), a browser-local image tool, and exported all four formats with the page defaults untouched.

Format Bytes Share of the original
Re-encoded GIF 983,087 62.02%
Animated WebP 1,045,924 65.98%
APNG 1,392,726 87.86%
Animated AVIF 157,490 9.94%

One 4.5-second screen recording stored five ways

My plan finished third out of four.

Why the GIF refuses to shrink

GIF saves bytes by redrawing only the part of the frame that changed, but it can only declare one rectangle per frame. In this animation the cursor moves on the left while the clock ticks in the top right, so that rectangle has to swallow both ends plus all the untouched pixels in between. 68 of the 75 frames do use partial refresh, and the dirty rectangle still averages 59% of the canvas.

Two more structural costs pile on: at most 256 colours per frame, with every frame after the first carrying its own local palette (74 palettes × 256 × 3 ≈ 57 KB of pure lookup tables), and LZW that only rewards horizontal repetition.

None of that explains why WebP lost, though. Judging from the output alone, I'd guess animated WebP also works in keyframes plus rectangular difference blocks — it inherits the same fat dirty rectangle and only saves on what happens inside it. With 59% of the canvas redrawn and small table text everywhere, there is no order-of-magnitude left to win. I haven't looked inside the encoder, so that's a guess from the artefacts.

APNG deserves its own sentence: its frames are pixel-identical to the source. It isn't a smaller format, it's a lossless one. On two other samples it came out 35% and 36% larger than the original GIF, and the tool says so on the result card.

Checking that nobody touched the timeline

Size was never the part that worried me. Dropped frames or rewritten frame delays quietly change the playback speed, and nobody notices until much later. So I ignored the summary in the UI and parsed the files myself. The APNG export was the easiest one to audit, because its per-frame delays sit in plain fcTL chunks:

from struct import unpack

def apng_frames(buf):                     # read the fcTL chunks of an APNG
    i, out = 8, []
    while i < len(buf) - 8:
        n, kind = unpack('>I', buf[i:i+4])[0], buf[i+4:i+8]
        if kind == b'fcTL':
            num, den = unpack('>HH', buf[i+28:i+32])
            out.append(1000 * num / (den or 100))
        i += 12 + n
    return out                            # 75 delays of 60.0 ms
Enter fullscreen mode Exit fullscreen mode

It returns 75 delays of 60 ms, 4500 ms in total; the 1280×800 demo gives 40 delays of 100 ms. GIF hides the same numbers in its graphic control extensions and animated WebP in its ANMF chunks, so each format needs its own reader. Across four samples and sixteen exports, frame count, per-frame delay, total duration and the infinite-loop flag all matched the source.

One quirk worth remembering: GIF stores delays in hundredths of a second, so only multiples of 10 ms survive. I asked for 66 ms while generating the sample and got 60 ms on disk. That's the format's resolution, not a compression loss.

What stopped me from switching everything to AVIF

The size argument is loud — the 1280×800 demo animation went from 1,127,031 bytes to 208,937, or 18.5% of the original. But I only put the WebP, APNG and AVIF files on one local page and opened it in Chromium 149, WebKit 26.5 and Firefox 151, screenshotting the same <img> twice 900 ms apart and comparing pixels to confirm it was genuinely animating rather than merely not erroring. All three engines passed.

Three desktop engines is all I have. In-app WebViews, older machines, whatever renders animations inside messaging clients — I tested none of them, so I can't speak for them. ImgIng's result card also notes that its animated AVIF carries no transparency, which is fine for screen recordings and not fine for a diagram on a transparent background.

So the plan turned into something duller: pick per asset, and keep a GIF fallback behind a <picture> for anything I haven't verified. I've only wired that markup up on a local page — how various docs pipelines treat it is something I haven't checked, so I won't claim it works everywhere.

One last counterintuitive bit. Another sample had already been squeezed my usual way — scaled to 70%, every second frame dropped, down to 64 colours — 357,648 bytes. Re-encoding it as GIF saved 684 bytes, 99.81%, pixel-identical. The same file as animated AVIF saved another 62%. "This one can't be compressed any further" only holds inside a single format.

Top comments (0)