Why GIFs Render Like Garbage (And How We Fixed Ours)
Published:
Author:Ryan Carter
Reading Time:6 Minutes

Every time we ship a paid social campaign that needs a GIF banner, the same thing happens. Someone exports frames out of After Effects or Figma, runs them through whatever free converter shows up first on Google, and the result looks like it was rendered through a fog. Skies become rough bands of blue. Skin tones go blotchy. The file is 1.8 MB so Meta rejects the upload. Everyone sighs and tries a different converter.
So we built our own. It runs in the browser, it dithers properly, and it tells you up front if the output is going to break the 1 MB ad platform limit. You can try it here. The rest of this post is why GIFs look the way they do and what we changed to get them looking right.
The format is from 1989 and it shows
GIF was designed when an 8-bit color screen was state of the art. The hard rule is that every frame can use at most 256 colors. Not 256 shades of each color. 256 total. That's the entire palette your sky, your skin, your logo, and your background all have to share.
When you export a JPEG, you're working with roughly 16 million possible colors per pixel. When you save the same image as a GIF, a converter has to pick 256 of those colors and throw the rest away. Every pixel then snaps to whichever of those 256 colors is closest. This step is called palette quantization, and it's where most of the damage gets done.
If the converter is lazy about it (and most of them are), you get visible bands wherever the original image had a smooth gradient. A blue sky that used to fade gently from light to dark becomes four or five flat blue stripes with hard edges between them. Skin tones that had subtle warm-to-cool transitions turn into uneven blotches. The image looks posterised because, technically, it is.
Why nearest-color matching makes that worse
The simplest way to map a pixel to a palette is: for each pixel, walk the 256 palette colors, pick whichever one is closest in RGB distance, and use that. Done.
The problem is that "closest" is binary. A pixel that's almost exactly between two palette colors still has to pick one of them, and every neighbouring pixel with a similar value will pick the same one. So you end up with whole regions of the image flipping to color A, then suddenly flipping to color B at the boundary. That boundary is the band you see in the output.
Photographs are the worst case. They're nothing but smooth gradients. Logos and flat illustrations survive quantization fine because they were already using a small number of distinct colors. Photos die.
Floyd-Steinberg dithering, which is what saves the day
Floyd-Steinberg is a 1976 algorithm that does one clever thing. When it picks a palette color for a pixel, it measures how wrong that choice was (the difference between the original pixel and the palette pixel it had to settle for), then spreads that error across the neighbouring pixels that haven't been processed yet.
The weights are specific. 7/16 of the error gets pushed to the pixel directly to the right. 3/16 goes diagonally down-left. 5/16 straight down. 1/16 diagonally down-right. The numbers aren't magic, they're just what Floyd and Steinberg found gave the best perceptual result.
The effect is that a smooth gradient stops being mapped to one palette color in big blocks. Instead, the algorithm bounces between two or three nearby palette colors in a fine pattern. Your eye blends the pattern back into the original gradient. The bands disappear. The image looks photographic again, just with a slight grain.
Our implementation goes one step further and uses a serpentine scan. Standard Floyd-Steinberg always processes pixels left to right, row by row. That creates very faint diagonal artifacts because the error always flows in the same direction. Serpentine scanning alternates: left to right on even rows, right to left on odd rows. The error diffusion mirrors with it, so the artifacts cancel out instead of accumulating.
You can see the result in our tool. Toggle the dither setting between "Floyd-Steinberg" and "None" on the same set of frames. With dither off, photo content looks like a poster. With dither on, it looks like a photo.
Use a per-frame palette, not a global one
Most GIF encoders generate one palette for the whole animation. If your first frame is a bright product shot and your fourth frame is the same product on a dark background, both frames have to share 256 colors. Neither one gets the colors it actually needs.
We generate a fresh palette per frame using pnnquant2 (the "pairwise nearest neighbour" quantizer from gifenc). The first frame writes its palette as the global table. Every subsequent frame writes a local color table that overrides it. The file gets slightly bigger because you're storing more palette data, but each frame gets to use its 256 colors on the colors it actually contains. The visual quality gain massively outweighs the file size hit.
This is why a four-frame banner where frame one is a sunny outdoor shot and frame four is a moody indoor shot now looks correct in both. Each frame is making its own decisions.
Downscale before you quantize, never after
This one trips up a lot of converters. If you have a 1920×1080 source and you want a 600×338 GIF, the order matters.
If you quantize first (pick 256 colors from the full-resolution image), then downscale the indexed result, the downscale step has to interpolate between palette indices, which makes no sense as a color operation. You get muddy averaging and the dithering pattern smears.
If you downscale first using the browser's bicubic-quality canvas filtering, then quantize the small image, every step is operating on real RGB values. The downscale produces clean averaged pixels, then the quantizer picks a palette suited to the actual output resolution.
Our tool does the second one. Always. The "Max output width" setting in the UI just shrinks the source canvas with imageSmoothingQuality: 'high' before any palette work begins.
The 1 MB cap that nobody warns you about
Meta caps GIF ad uploads at 1 MB. Google Display Network caps animated banners at 150 KB for most slots, with some up to 1 MB. LinkedIn is around 5 MB. TikTok doesn't really use GIFs.
The 1 MB Meta limit is the one that bites us weekly. You design a beautiful four-frame banner, render it, and the upload fails after thirty seconds of waiting because the file is 1.04 MB. Now you're back in the converter trying to figure out what to cut.
Our tool calculates the output size after every change to settings and shows it in big numbers, green if you're under the limit, yellow if you're over. If you're over, it suggests a max-width that should bring you back inside the cap based on the ratio of how far over you are. The formula is simple: file size scales roughly with pixel count, so if you're 50% over you want to shrink width by sqrt(1.5) which is about 18%. Crude but it gets you in the right ballpark first try.
The reason we suggest reducing width before reducing colors is that detail almost always matters more than palette depth for ad creative. A 256-color GIF at 480px wide looks better than a 32-color GIF at 600px wide. Try it yourself if you don't believe us.
What we cut
The version of this tool we use internally walks an entire folder structure and writes GIFs back into each subfolder with structured filenames, because that's how our production projects are organised. We stripped all of that out for the public version. It would have meant asking for write access to your filesystem (which only works in Chrome and Edge anyway) and it solves a problem you don't have.
The public version does one thing. You drop a handful of images of the same dimensions, you adjust settings if you want to, and you download the result. No accounts. No uploads. No file ever leaves your browser.
Try it
The tool is at studiofuture.ae/labs/gif-maker. Pick four or five same-sized JPEGs (export them from Figma at 600×500 or whatever your placement needs), drop them on the dropzone, hit download. If you've been suffering through online GIF converters for ad creative, you'll feel the difference immediately.
We also built it because we needed it to work for us, not for some imagined enterprise customer. It's a single Vue page, a port of the brilliant gifenc library, and about 80 lines of dithering code. Everything we learned figuring out how to make it look right is in this article. Have at it.
Got a broken creative workflow that wants fixing? This kind of tool is a side effect of running paid campaigns at volume and getting tired of the existing options. If you've got a similar friction point in your stack, we'd love to hear about it.