Comparing the app size & startup performance of the same app in native (Compose/Swift UI), Flutter and Kotlin/Compose Multiplatform (KMP) on Android and iOS, by using cat pictures. Or, more specifically, by creating the exact same app three times.
This article is a follow-up to Flutter versus Kotlin Multiplatform for existing codebases, this time focusing only on performance & app size.
🐈 The sample app
The app will contain a single screen, load cat pictures from a public API and show them in a horizontal list. Each picture can be clicked to show it zoomed in below the list. For the native versions I used, of course, Compose and SwiftUI.
📂 Technology overview
All code can be found in this repository: https://github.com/jacobras/flutter-vs-native-vs-kmp.
|Folder||Technology||UI Framework||Network lib||Image lib|
|Cross-platform, Dart||Flutter||http.dart||Built-in image widget|
|Cross-platform, Kotlin||Compose UI Multiplatform||Ktor||Kamel|
|Native, Kotlin||Compose UI||Ktor||Kamel|
📊 Benchmark results
📦 App size
Android sizes were measured by running Bundletool on the signed, minified release bundles to create an APK set for a Pixel 4a, and then running
get-size on that APK set. iOS sizes were measured running the App Thinning Size report.
For Android, the native app was just 1.463 MB. The KMP/Compose app came to exactly the same size, which is unsurprising because it’s just multiplatform code with an extra iOS target, which is not included in the Android built, resulting in the same end result. The flutter app came to almost 5 times the size.
On iOS, the differences are bigger. The native app depends on SwiftUI, which is bundled with the OS. This makes the app only 1.7 MB, almost as small as the native Android app. The KMP/Compose variant is quite bigger at 24.8 MB. I asked about this in the Kotlin Slack channel and the explanation seems to be the bundling of Skia. Compose needs to be bundled for both platforms, but on Android, Compose “relies on the built-in Skia lib shipped with Android” whereas on iOS the Skia library needs to be shipped too. The same goes for Flutter, although it’s smaller at a total size of 17.9 MB (for iOS 12.2+, builds for earlier versions are 25.4 MB).
Note: updating Compose from 1.4 to version 1.5 (with a bunch of iOS improvements) further increases the IPA size to 32.2 MB. Keep in mind we’re dealing with an alpha version.
🚀 Startup time
Android startup benchmarks were executed using Macrobenchmark, 5 times on a Pixel 4a. Test suite was run twice in alternating order. Shown below is the median startup time. Min. and max. are not much different, I’ve included them in the repository at https://github.com/jacobras/flutter-vs-native-vs-kmp.
Compose Multiplatform’s startup time should be the same as native, as the apps are almost completely identical. They almost are, with just a 12 ms difference. In the Flutter app, we can clearly see the penalty for having to start up the Flutter engine, slowing down the app by 221.1 ms (1.5 times slower). This matches what the Flutter docs are saying about it.
The iOS startup tests were run on an iPhone 12 Mini. Both Compose Multiplatform and Flutter add 12% to the startup time, although it’s relatively less than Flutter on Android (which adds 54%).
This comparison showed some interesting results. Seeing the native Android & KMP Android builds ending up at exactly the same size was not unexpected, but is a nice reminder that adding KMP doesn’t increase Android app size. Adding KMP to an iOS app adds less than half a megabyte, but when pairing it with Compose UI it adds 23.1 MB in total. That’s a slightly bigger number than what Flutter added to the iOS app: 17.9 MB.
Please note that Compose for iOS is, as of the time of writing, only in alpha. Version 1.4 and 1.5 were used. Hopefully we’ll see additional improvements as it matures.
Thanks to my iOS colleagues Matt and Michael for helping me with the iOS technicalities of this post.