Common mistakes with Images in Flutter

Roman Ismagilov
4 min readJun 10, 2024

--

Images are one of the core features that almost every app uses. Yet, there are several very common ways to affect the application performance by making mistakes that could be easily avoided. Let’s have a look at some of them.

1. Large image assets

This can take a lot of memory and processing time. Decoded bitmap size in memory directly depends on their decoding resolution, and that can greatly impact the app’s performance.

Reduce the size of bundled image assets

Most of smartphones have screen widths no larger than 1200 pixels, so it makes sense to scale assets accordingly. Let’s take as an example a 7500x5000 pixels photo. The amount of used RAM to store the bitmap of such size is 112 Mb, which exceeds the default size of the image cache in Flutter. The rule of thumb here will be that the sum of bitmap sizes should not exceed 100 Mb for a single app session. Otherwise, images will be decoded again, resulting in a less smooth user experience.

By reducing the image resolution to 1200x800 the bitmap size goes down to 2.8 Mb. Calculations are done using this tool.

However, we can do that only when we have control over the files, but what if the images come from a remote source? Or what should we do if the same image could be used in different layouts with different sizes?

Use cacheWidth and cacheHeight

By providing these parameters we can specify the decoding size of the image. Don’t forget to include the MediaQuery.of(context).devicePixelRatio in the calculations. Note, that the same image assets with different cacheHeight/cacheWidth would be considered different images in the cache.

    Image.asset(
"assets/6392956.jpg",
height: 100,
width: 300,
cacheHeight: (100 * MediaQuery.of(context).devicePixelRatio).toInt(),
);

To test how this affects cache let’s do some measurements. The current cache size can be accessed through the PaintingBinding class:

PaintingBinding.instance.imageCache.currentSizeBytes;
The test app was given a fresh restart every time to exclude irrelevant items in the cache

As expected, larger images have the cache size increased. However, the original image is so large, that it was not added to the cache at all and that results in decoding the image every time it is displayed. Let’s see how it looks:

In this video, the image that exceeds the cache size limit is shown with a delay every time the containing screen is opened, while for a resized image, the delay is shown only when the screen is opened for the first time.

2. Not using WebP assets

Another optimization of bundled assets is using WebP format. It can significantly reduce the file size of images. There’s a plenty of free online converter tools and Flutter supports WebP out of the box.

3. Using the Opacity widget when not needed

Opacity widget is very useful and convenient, yet it should not be used any time we want, because it creates a new rendering layer every time it is used. Let’s see what happens if this widget is included several times on a screen:

    Opacity(
opacity: 0.5, // <- avoid doing like that whenever possible
child: Image.asset(
"assets/6392956.jpg",
height: 100,
width: 300,
),
);

Then we open the DevTools and check the render layers:

Every layer is rendered independently, which causes a lot of excess calculations. Instead of using the Opacity widget, we can blend a color with an image, as recommended in the documentation:

Image.asset(
"assets/6392956.jpg",
height: 100,
width: 300,
color: Colors.white.withOpacity(0.5), // <- this
colorBlendMode: BlendMode.modulate, // <- and this
cacheHeight: (100 * MediaQuery.of(context).devicePixelRatio).toInt(),
),

This way we have all the images on the same rendering layer:

The same applies to using theColorFiltered widget, better to use theImage’s color + colorBlendMode combination whenever possible, since this avoids creating a new composited layer.

4. Not precaching image assets

Flutter provides us with the possibility to manually push images to ImageCache. Let’s try to do it with one of the previous images:

TextButton(
child: const Text("Precache image"),
onPressed: () async {
cacheSize() => PaintingBinding.instance.imageCache.currentSizeBytes.toString();
print(cacheSize());
final asset = Image.asset(
"assets/6392956.jpg",
height: 100,
width: 300,
cacheHeight: (100 * MediaQuery.of(context).devicePixelRatio).toInt(),
);
await precacheImage(asset.image, context); // <- precaching
print(cacheSize());
},
)

The output:

As a result, when we open the screen that contains this image with the same decoding size, it will be opened immediately, unless removed from the cache. Use this technique cautiously, as the size of the cache is limited.

5. Not caching network images

If your app fetches images from the network, it would make no sense to load these images every time. Instead, we can use the cached_network_image library or any other alternative. The library documentation is pretty self-explanatory.

Hope you’ve found this article useful. I will update it with more techniques whenever I find something useful. Follow me on Twitter to get the latest updates. If you want to read the full code, you can check the repository.

--

--

Roman Ismagilov

Covering some non-obvious nuances of Flutter development in my articles