Optimizing performance for images

Working with images can quickly introduce performance issues if you aren't careful. You can quite easily run into an OutOfMemoryError when working with large bitmaps. Follow these best practices to ensure your app performs at its best.

Only load the size of the bitmap you need

Most smartphones have high resolution cameras that produce large image files. If you're showing an image on screen, you must either reduce the resolution of the image or only load the image up to the size of your image container. The constant loading of larger than needed images can exhaust GPU caches, leading to less performant UI rendering.

To manage image sizes:

  • Scale down your image files to be as small as possible (without affecting output image).
  • Consider converting your images to WEBP format instead of JPEG or PNGs.
  • Supply smaller images for different screen resolutions (see Tip #3),
  • Use an image loading library, which scales down your image to fit the size of your view on screen. This can help improve the loading performance of your screen.

Use vectors over bitmaps where possible

When representing something visually on screen, you need to decide if it can be represented as a vector or not. Prefer vector images over bitmaps, as they don’t pixelate when you scale them to different sizes. However, not everything can be represented as a vector - images taken with a camera can’t be converted into a vector.

Supply alternative resources for different screen sizes

If you are shipping images with your app, consider supplying different sized assets for different device resolutions. This can help reduce the download size of your app on devices, and improve performance as it’ll load up a lower resolution image on a lower resolution device. For more information on providing alternative bitmaps for different device sizes, check out the alternative bitmap documentation.

When using ImageBitmap, call prepareToDraw before drawing

When using ImageBitmap, to start the process of uploading the texture to the GPU, call ImageBitmap#prepareToDraw() before actually drawing it. This helps the GPU prepare the texture and improve the performance of showing a visual on screen. Most image loading libraries already do this optimization, but if you are working with the ImageBitmap class yourself, it is something to keep in mind.

Prefer passing a Int DrawableRes or URL as parameters into your composable instead of Painter

Due to the complexities of dealing with images (for example, writing an equals function for Bitmaps would be computationally expensive), the Painter API is explicitly not marked as a Stable class. Unstable classes can lead to unnecessary recompositions because the compiler cannot easily infer if the data has changed.

Therefore, it is preferable to pass a URL or drawable resource ID as parameters to your composable, instead of passing a Painter as a parameter.

// Prefer this:
@Composable
fun MyImage(url: String) {

}
// Over this:
@Composable
fun MyImage(painter: Painter) {

}

Don’t store a bitmap in memory longer than you need it

The more bitmaps you load into memory, the more likely it is that you could run out of memory on the device. For instance, if loading a large list of Image composables on screen, use LazyColumn or LazyRow to ensure that memory is freed up when scrolling a large list.

Don’t package large images with your AAB/APK file

One of the top causes for large app download size is due to graphics that are packaged inside the AAB or APK file. Use the APK analyzer tool to ensure that you aren't packaging larger than required image files. Reduce the sizes or consider placing the images on a server and only downloading them when required.