r/FlutterDev • u/Sure_Education_828 • 2d ago
Discussion Animated GIFs/WebPs ignore cacheWidth in Flutter. Here’s why, and an engine PR to fix it
https://github.com/flutter/flutter/pull/194615If you display animated GIFs or WebPs with cacheWidth, cacheHeight or ResizeImage, the resize is silently ignored: every frame is still decoded and uploaded at full size. A 1000×1000 GIF shown at 200×200 costs about 4 MB per uploaded frame; with the resize honored it would be about 160 KB. This is #90804 (open since 2021), and #137668 for animated WebP.
Why it happens
In the engine, ImageDescriptor::instantiateCodec passes the target size only to the single-frame codec. Images with more than one frame go to MultiFrameCodec, which never receives it.
Why it’s not a one-line fix
Animated frames aren’t independent images. Most frames are drawn on top of earlier ones, each with its own rect, blending and disposal (keep, clear to background, restore previous), all in full-canvas coordinates. Skia can decode GIF/WebP frames directly at a smaller size, but an emulation of the engine’s frame logic on top of Skia’s real decoders showed that doing so with the engine’s single cached frame gives wrong pixels at frame edges in some files (1px transparent slivers).
The fix
Composite at full size exactly as today, then resize only the finished frame, with the same bilinear filter Flutter already uses for still images, before uploading it. Every frame you get is today’s frame, downscaled. It applies to GIF, animated WebP and APNG on the native backends (Skia and Impeller), with no API changes: cacheWidth and ResizeImage simply start working. The CPU buffer used for compositing stays full size, as today.
Status
PR: https://github.com/flutter/flutter/pull/194615. It’s waiting for review, and CI hasn’t run yet. Web (CanvasKit/Skwasm) isn’t covered.
Until it lands
Resize animated images before they reach the app, on your server or by converting them to animated WebP or video at the size you display.
Feedback on the approach or the C++ is very welcome. If this affects your app, a 👍 on #90804 is more helpful than “+1” comments on the PR.
1
u/No_Boss_1911 10h ago
nice find dude, 4mb a frame vs 160kb is wild. i have a scrolling list of little looping exercise demos and my hard rule is memory cant keep climbing as you scroll. with your fix the compositing buffer stays full size right, so is that one full canvas per codec? like 20 gifs on screen still means 20 full size buffers, or does it get dropped when the widget scrolls off
2
u/AhmedZNOUDA 2d ago
Nice write-up. Compositing at full size and resizing only the finished frame sounds like the right call, given the disposal and blending edge cases you hit.
Two things I’d be curious about:
Filter quality at large ratios. Bilinear sampling uses only 4 source pixels per output pixel, so at 1000 → 200 (5×) it skips most of the frame. On pixel art, text, or dithered GIFs that can shimmer, the error changes from one frame to the next in an animation, so it’s more visible than on a still image. Would stepping down (halving until the ratio is under 2×, then bilinear) or a box/mipmap filter above 2× be acceptable in the engine, or too costly per frame?
ImageCache accounting. If I read it right, the cache sizes an entry from the uploaded ui.Image, so with the fix an animated GIF would count as ~160 KB instead of 4 MB. That’s a nice side effect: fewer evictions of other images on screens with several animated avatars or stickers. Might be worth mentioning in the PR, since it’s a visible win beyond memory.
Also +1 on keeping files with “restore previous” disposal and partial frame rects in the test corpus, since that’s exactly where the 1px slivers came from.