DEV Community

Max/Wang
Max/Wang

Posted on

I put gifsicle in the browser to build a GIF compressor

I run SquishyFile, a small site of free video tools, and I recently added a GIF compressor to it. It runs gifsicle, Eddie Kohler's GIF optimizer, compiled to WebAssembly inside a Web Worker. You drop in a GIF, pick a target size or a few settings, and get a smaller GIF back. All of the work happens on your own device, so there's no upload and no queue.

This post covers how I picked the defaults, two bugs I hit when dropping frames, and how the tool hits a size like 256 KB when gifsicle has no flag for that.

Why gifsicle

GIF compression is an old problem, and gifsicle has handled it well for a very long time. The part I cared about most is its lossy mode. GIF uses LZW, which loves long runs of identical pixels and hates noise. Lossy mode lets the encoder swap a pixel for a close neighbor color when that creates a longer run. On camera footage you see a bit of grain. On the file size you see a big drop.

I used the gifsicle-wasm-browser package. It ships its worker as a source string with the .wasm inlined as base64, so nothing loads from a CDN at run time. The module weighs about 340 KB, and most people who open the page never press Compress, so I only import it on the first click.

Before I picked defaults I ran some tests. On a 36 MB GIF (800px wide, 120 frames), -O3 gave me the same size as -O2 and took about three times longer. So every level uses -O2, and only the lossy value changes: 30 for Light, 80 for Balanced, 140 for Strong. Balanced took that 36 MB file down to about 17 MB in roughly 5 seconds.

What each setting buys you

The tool has four knobs: compression level, colors, max width and frames. I ran them on a 3-second, 480×270 sample with 36 frames that starts at 4.0 MB:

Settings Result Saved
Light 2.5 MB 38%
Balanced 1.9 MB 53%
Strong 1.6 MB 59%
Balanced + 64 colors 1.5 MB 63%
Balanced + 360px wide 1.1 MB 74%
Strong + 64 colors + 360px + every 2nd frame 343 KB 92%
Balanced + 64 colors + 240px + every 2nd frame 206 KB 95%

Compression alone got this GIF to about half its size. To get it really small I had to cut width and frames too. Width saves the most because halving it leaves a quarter of the pixels, but it's also the change people notice first.

Dropping frames broke two things

Keeping every 2nd or 3rd frame sounds simple. My first version just picked frames 0, 2, 4 and so on, and it went wrong in two ways.

The GIF played at double speed. Each GIF frame carries its own delay. Throw away half the frames and keep the delays as they are, and a 3-second loop finishes in 1.5 seconds. It looked like fast-forward. Now each kept frame takes over the delays of the frames I drop after it:

export function keptFrames(delaysCs: number[], step: number) {
  const kept = [];
  for (let i = 0; i < delaysCs.length; i += step) {
    let delay = 0;
    for (let j = i; j < Math.min(i + step, delaysCs.length); j++) delay += delaysCs[j];
    kept.push({ index: i, delayCs: delay });
  }
  return kept;
}
Enter fullscreen mode Exit fullscreen mode

Frames came out half drawn. Optimized GIFs, including plenty you'll find online, store most frames as a patch over the previous frame. When I picked frame 2 and skipped frame 1, frame 2 got painted over a frame that wasn't there anymore, and I got smeared, broken images. So frame dropping now runs in two passes. The first pass runs gifsicle with -U to turn every frame back into a full image. The second pass picks the frames, compresses them and optimizes again.

One more gifsicle detail bit me here. A -d delay flag applies to every frame after it until the next -d. So as soon as any kept frame has a delay, I write an explicit -d in front of every kept frame, even when the value is 0.

The unoptimize pass also prints "GIF too complex to unoptimize" on almost every run, even when the output is fine. I compared the results frame by frame against the source to make sure, then filtered that line out of the console so it doesn't scare anyone who opens DevTools. Real failures still show up, because they produce no output file and the promise rejects.

Hitting a size target

Most people compress a GIF because something rejected it. A Discord emoji slot wants 256 KB, a chat app or forum has its own cap. So the tool has a "Fit under a size" mode with presets from 256 KB to 10 MB, plus a custom value.

gifsicle can't aim at a byte budget, so the tool searches. It walks a ladder from gentle to harsh and stops at the first result that fits:

  1. Light, Balanced, Strong
  2. Every 2nd frame
  3. 128 colors, then 64
  4. Width, last

Every step is a full gifsicle run, so I didn't want to run all of them every time. I measured how much each step usually shrinks a file compared to the step before it. Balanced comes out around 0.76 of Light, Strong around 0.86 of Balanced, and dropping half the frames around 0.55. The tool uses those factors to estimate the next result and skips steps that will clearly still be too big.

For width I stopped stepping down blindly. GIF size roughly follows pixel count, so the tool solves for the width it needs:

const next = Math.floor(width * Math.sqrt(budget / measured) * 0.97);
Enter fullscreen mode Exit fullscreen mode

If that lands far under the budget, it spends one more pass on a wider version, so you don't get a GIF smaller than it had to be. It also aims at 98% of the target, because sites don't always count bytes the same way. Most fits finish in 2 to 5 passes.

Smaller things

  • Reading the GIF. I wrote a tiny parser that walks the GIF block structure to get width, height, frame count and each frame's delay. It never decodes pixels, so even a 50 MB GIF reads instantly.
  • Cancel. The package's own run() helper owns its worker and you can't stop it. I start a worker from the same source myself, so the Cancel button can call terminate().
  • Progress. gifsicle reports nothing while it works, so the progress bar runs on a time estimate. I measured about 7 MB of GIF per second and let the bar park at 95% until the worker answers.
  • Bigger output. On some already tight GIFs the result comes out larger. In that case the tool tells you so and doesn't offer the bigger file as a download.

Try it

The tool lives at squishyfile.com/compress-gif. If you have a GIF that comes out weird or barely shrinks, send it my way. Those are the ones I learn the most from.

Top comments (0)