DEV Community

Cover image for Hitting an exact GIF file size in the browser without a slow retry loop
Siva Bollin
Siva Bollin

Posted on

Hitting an exact GIF file size in the browser without a slow retry loop

Most GIF compressors give you a quality slider and leave you to guess. That is a bad fit for the usual reason people shrink a GIF: a platform has a hard limit. Discord emoji and Slack uploads come with hard size and frame limits. You do not want "roughly smaller", you want "under N bytes, as good as possible".

I built a browser-only GIF compressor around that idea. You enter a target size, and it works out the settings. Everything runs on the user's machine. The encoder is gifsicle compiled to WebAssembly, running in a web worker so the page stays responsive. This post is about the search problem on top of it, because that is where the interesting decisions were.

The problem: encodes are slow, the search space is big

The knobs that change output size are width, frame count, color count, and lossy strength. A naive approach is a binary search on one knob. But each full-size encode in WASM is slow (a couple of seconds on my machine for larger GIFs), so ten iterations is already a bad experience, and one knob is not enough.

Step 1: try the cheap knobs first, in order of damage

The search goes from least to most visible damage:

  1. Lossy compression alone, from gentle to strong.
  2. Fewer colors (128, then 64, then 32).
  3. Shrinking the width, and if that would cut the width too far, dropping frames first.

Before walking each ladder, it encodes once with the strongest settings for that step. If even the strongest setting misses the target, the gentler ones cannot fit either, so the whole ladder is skipped. That one check saves several full-size encodes in the common case where the target is far away.

Step 2: a half-width probe

For the resize step, I do not want to run slow full-size encodes just to find out that none of them fit. So the first encode is at half the width, which is about four times fewer pixels and much faster. That single probe gives a size at a known width.

From experience with gifsicle on my own sample GIFs, output size scales roughly like this:

bytes ∝ width^1.85 × frames^0.85
Enter fullscreen mode Exit fullscreen mode

Those exponents are empirical, fitted on my samples with -O3 --lossy=120 --colors=64, not a law. They are good enough to turn the probe into a starting guess:

function predictWidth(probeWidth, probeBytes, targetBytes, frameStep) {
  const frameFactor = Math.pow(frameStep, -0.85);
  return probeWidth * Math.pow(targetBytes / (probeBytes * frameFactor), 1 / 1.85);
}
Enter fullscreen mode Exit fullscreen mode

The same probe also answers a cheaper question first: could any full-size setting plausibly fit? If the predicted full-size output, even after halving it for the strongest color and lossy settings, is still over the target, the full-size ladders are skipped entirely.

Step 3: a bracketed search that refines the model as it goes

The estimate is only a guess, so it is refined with a short search. It keeps a bracket: the widest width known to fit and the narrowest known to miss. After each encode it fits the exponent again from the last two measurements (a log-log secant step), and falls back to 1.85 if the slope looks implausible. It aims about 2% under the target so the next attempt tends to land inside the budget, and it stops when another encode would gain less than about 3% width.

let slope = 1.85;
if (prev && prev.width !== width && prev.bytes !== output.length) {
  slope = Math.log(output.length / prev.bytes) / Math.log(width / prev.width);
  if (!Number.isFinite(slope) || slope < 1 || slope > 3) slope = 1.85;
}
next = width * Math.pow((targetBytes * 0.98) / output.length, 1 / slope);
Enter fullscreen mode Exit fullscreen mode

In practice this converges in a handful of encodes. I have not benchmarked it against a plain binary search, so I will not claim a specific speedup, only that the starting point is much closer than the midpoint.

Dropping frames without making the animation faster

Sometimes shrinking width alone would blur the GIF too much. Dropping frames is the other lever, but a naive "delete every second frame" halves the duration, so the animation plays twice as fast.

The fix is to add the delay of each removed frame to the frame that is kept. If you keep every other frame, each kept frame stays on screen for its own delay plus the delay of the frame that was dropped after it. The total duration stays close to the original, and the function reports the remaining error so it can be checked in tests.

Two details bit me here:

  • GIF delays are in hundredths of a second, so every compensated delay has to be rounded. Each kept frame gets the rounded sum of its group, so the error stays small, but it is not zero.
  • Browsers play delays of 0 to 10 ms as 100 ms. If you compute the frame rate from the raw numbers, a GIF with many zero delays looks much faster than it plays, and you drop frames you could have kept. The fps estimate treats those delays as 100 ms.

There are limits on how far it goes: it never goes below 6 fps from optional frame dropping, never drops beyond keeping every third frame (unless a platform frame cap forces more), and prefers dropping frames over shrinking below half the original width.

Platform rules go in before the search. A frame cap forces a minimum frame step, and the Discord emoji preset is bounded to a 128x128 box and about 250 KB, so the candidates respect those limits from the start.

A WASM gotcha worth knowing

Calling gifsicle's CLI entry point repeatedly in one module instance worked until one call failed. After a failed run its global option state stayed dirty and every later call failed too. The fix is to throw away the cached module whenever an exit code is non-zero and create a fresh one on the next task.

What I have not verified

I have not compared compression ratios or speed against other tools, and the size model comes from my own sample set, so it may be off on unusual GIFs (huge flat-color animations, for example, or screen recordings). If you have a GIF that lands far from its target, I would like to see it.

The tool is free, and nothing is uploaded: https://compressgifonline.com

Top comments (0)