Benchmarking Image Loading Libraries on Android

Colin White
The Startup
Published in
4 min readOct 22, 2020

--

Image loading libraries make it simple to fetch, decode, and display images. On Android, it’s very likely you’ll need to use one as the Android SDK mostly only exposes low-level APIs like BitmapFactory and ImageDecoder to load images. These APIs accept an InputStream or a Uri however threading, memory and disk caching, request management, memory management, opening/closing any resources, and other low-level concerns are left up to the developer to handle. The lack of high-level, simple APIs to load images makes it especially tough to load images from the network, which is a large reason why developers use open source image loading libraries.

Each of the 4 main image loading libraries on Android (Coil, Fresco, Glide, and Picasso) has different design considerations and features. However, what if we wanted to compare their image decoding speed? The first thing to note is all the image loading libraries use the same low-level APIs to fetch and decode images. Every library uses BitmapFactory to decode image data. Likewise, every library uses OkHttp or HttpUrlConnection (though you should really use OkHttp) for its networking. Furthermore, each library uses the same Android SDK APIs to read image data from a Uri or File. The takeaway is that any performance differences between these libraries is largely due to the amount of overhead a library introduces by wrapping those API calls in a nice, high-level API.

Benchmarks

These results were measured on a Pixel 1 running Android 10. The benchmarks use the AndroidX benchmarking library which runs the device in sustained performance mode, calls a function many times, and prints the median execution duration. The benchmarking code is available here and the raw results are available here. To reduce confounding variables, all libraries are using OkHttp for their networking. Also, I included a control variant in the benchmarks that manually opens an InputStream from the network/file Uri and decodes it directly using BitmapFactory.decodeStream. This variant should always perform best as it has no overhead. The versions of the libraries benchmarked are Coil (1.0.0), Glide (4.10.0), Fresco (2.3.0), Picasso 2 (2.8), and Picasso 3 (2832bcd). Lastly, it’s important to note that I wrote most of Coil and am not impartial.

As expected, the control variant out performs all the image loading libraries. Picasso 2 and 3 both perform very well - especially for the file benchmark. Glide and Coil perform similarly on the network benchmark, however Coil is slightly faster at loading an image from a file. Fresco is the slowest, though it’s possible it’s not optimized for this benchmark. Typically, you don’t access the bitmap directly with Fresco and instead render an image directly with a SimpleDraweeView. This benchmark misses any optimizations inside of SimpleDraweeView.

Now, let’s take a look at the benchmarks after each library is run through Google’s R8 code optimizer. As release builds are typically built with R8 enabled, this should offer a better view of the library’s performance in production.

Overall, we see very similar results to the non-R8 benchmarks with a 0–8% performance improvement depending on the benchmark.

Lastly, I wanted to compare the code size of each library inside the benchmarking APKs - not including transitive dependencies. This was measured using Android Studio’s APK analyzer. Note that “Fresco (Native)” includes at minimum 168.5kb of native code, which was added to its size.

Picasso 3 is the clear winner here as it’s only 36.2kb after R8 processes it. Coil is only 94.6kb which is significantly smaller than Glide (222.2kb) and Fresco without native code (244.1kb).

Conclusion

Picasso (2 or 3) clearly came out ahead in the benchmark. It’s the smallest library and it also ran the image decode benchmarks the fastest. However, this benchmark doesn’t capture everything you should consider when choosing an image loading library. For instance, this benchmark doesn’t track memory allocation. Unlike the other libraries, Picasso doesn’t resize images exactly when it decodes them into memory; this will allocate larger than necessary bitmaps. Also, Picasso is missing some features that the other libraries support such as animated GIF support, direct memory cache access, custom transitions, and bitmap pooling.

Again, I’m biased, but I believe Coil offers the best balance of performance, size, and features while being the only image loading library to integrate with Kotlin and Coroutines. Coil came second in the benchmarks and supports most of the features Glide supports as well as some it doesn’t (direct memory cache access, interceptors, event listeners). Also, Coil is designed with Jetpack Compose in mind and will have first party support for it once it is API-stable.

Questions/comments/feedback? Shoot me a message me on Twitter.

--

--