Dev.to WebDev πŸ›  Dev πŸ‘ 0 πŸ“– 4 min read

Compressing and resizing GIFs entirely in the browser

Compressing and resizing GIFs entirely in the browser Chat apps still treat GIF as a first-class attachment, and they still enforce hard upload limits. The usual fix is to send the file to an online compressor. That wo

Compressing and resizing GIFs entirely in the browser

Chat apps still treat GIF as a first-class attachment, and they still enforce hard upload limits. The usual fix is to send the file to an online compressor. That works, but it is a lot of trust for a bug-report clip or a meme you would rather keep on the device.

You do not need a server for this. Decode the palettized frames into RGBA, transform them, and encode again β€” with GIF's 10-millisecond delay precision β€” and the whole job can stay inside a browser tab.

Keep the UI thread out of it

The practical constraint is memory and main-thread jank, not network. Expanding a large, many-frame GIF into full canvases can allocate hundreds of megabytes. I run the work in a module Web Worker: the page posts a File, receives progress events, and gets back a Uint8Array that becomes an object URL.

On the way in, gifuct-js parses the file and I composite every frame onto a full-size canvas, honoring disposal 2 (clear the dirty rectangle) and disposal 3 (restore the saved canvas). After that, every frame is a complete RGBA buffer, which makes resize, crop, and frame-dropping straightforward.

Two guardrails matter on phones. First, I cap total pixels across all frames β€” 60 million in this pipeline, about 240 MB of RGBA β€” and, if a file is over budget, downscale while decoding. Second, I clamp frame delays to a 20 ms minimum. Many browsers, and clients such as Discord and Slack, treat a delay under 20 ms as 100 ms. Honor a file's 10 ms delays when speeding a clip up, and the export can play slower than you intended.

const MIN_SAFE_DELAY = 20; // shorter delays often play as 100 ms

function roundDelay(ms) {
  return Math.max(MIN_SAFE_DELAY, Math.round(ms / 10) * 10);
}

When a speed change would push a frame below that floor, I merge it into a neighbor and carry the leftover time.

Encoding only what changed

Encoding uses a vendored copy of gifenc: a per-frame palette of 2–256 colors, then LZW. Opaque animations get a useful extra step. After the first full frame, each later frame is compared against the pixels already on screen β€” the quantized result, not the pre-palette source, so rounding errors do not accumulate. Only the bounding box of changed pixels is written. Unchanged pixels inside that box are marked transparent so the previous frame shows through. If nothing changed at all, I write a 1Γ—1 transparent pixel just to preserve the delay.

Animations that already contain transparency cannot use that trick safely. They are written as full frames with disposal 2, so holes stay holes even if the file grows. A diffTolerance lets nearby RGB values count as unchanged: 0 is lossless deduplication; higher values trade a little smearing for a smaller dirty rectangle.

Resize is not a single algorithm

Shrinking and enlarging need different filters. When the output is smaller or the same size, I use area averaging with premultiplied alpha so thin transparent edges do not go muddy. When enlarging, I use bilinear sampling. A square crop is a centered cut with no movable box β€” fine for avatars, easy to clip a waving hand if you skip the loop preview. Changing dimensions does not guarantee a smaller file, which is why a compressor and a resizer should be different tools: one asks for a width (128–800 px presets, or an exact size up to 4,000 px), the other asks for a byte target.

A ladder, not a single knob

To aim at 512 KiB or 1 MiB, I do not expose a vague β€œquality 70.” I walk a fixed ladder and stop at the first encode that fits: fewer colors (256 down to 32), a smaller scale (100% down to 30%), then fewer frames (keep every frame, then every second, third, or fourth), while also raising the inter-frame tolerance. Dropped frames donate their delay to the frame that is kept, so the decoded duration stays the same even if the motion gets choppier.

If no step fits, the tool returns the smallest candidate and says so. A 30%-wide, 32-color, every-fourth-frame export of a long, grainy clip can still miss a 256 KiB budget. Trim the clip or resize to the display size first β€” smaller dimensions usually look better than heavier color reduction. Flat colors also compress far better than film grain or camera shake.

The same worker converts WebP. Still images go through createImageBitmap. Animated WebP needs the ImageDecoder API so every frame is recovered; I also inspect the RIFF chunks for ANIM / ANMF (or the VP8X animation flag). If the file is animated and the browser cannot decode frames, conversion stops rather than exporting only the first frame.

If the destination accepts MP4, compare a video export as well. Google's notes on replacing GIFs with video are a good reminder that GIF is a convenience format, not a compression format.

I wired this pipeline into the free browser tools on ImageToGif, including a GIF compressor that aims at a size you choose. The file never leaves the tab, there is no account, and nothing adds a watermark. I would like to hear if you have hit a 10 ms delay that plays as 100 ms.

πŸ“° Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes β€” full credit and traffic to the original publisher.