Android App Startup Tip: Don’t Use Kotlin Coroutines

Did you know that there is a significant initialization cost to Kotlin coroutines? Well, maybe not significant in all cases, but when it comes to app startup, every millisecond boost is worth taking. I found that simply launching the first coroutine can take over 100ms. 😱
Most applications perform a variety of tasks on startup: initializing third-party libraries, setting up services or periodic jobs, making HTTP requests to fetch data… Some must happen synchronously on the main thread, the rest can be offloaded to background threads in order to speed up the app’s startup and keep it responsive. With Kotlin coroutines being officially recommended for background processing on Android, it would be tempting to use them during startup as well, for example:

On a Moto G6 running Android 9—a low-end device by 2020 standards—the line CoroutineScope blocks the main thread for 110 ± 18ms on average (n=10, coroutines version 1.4.2), regardless of the contents of the coroutine itself. Let’s see what’s going on. Here is a trace captured using the Android Profiler:

https://www.reddit.com/r/topnflstreams/
https://www.reddit.com/r/topnflstreams/comments/kwne8e/nfl_streams_reddit_nfl_streams/
https://www.reddit.com/r/topnflstreams/comments/kwng6z/watch_2021_nfl_divisional_round/
https://www.reddit.com/r/topnflstreams/comments/kwnhu8/nfl_streams_reddit_where_to_watch_2021_nfl/
https://www.reddit.com/r/topnflstreams/comments/kwnii3/nfl_streams_reddit_1712021/

About 15% of the time is spent creating the CoroutineScope. 30% is spent creating Dispatchers.Default. And 55% is the actual launch call. So there isn’t any single cause. We can see kotlin.random.Random. (the static initializer) taking a large proportion of the launch call, perhaps that could be avoided. But as users of the public API it’s not clear what we could do here.
Prior to Kotlin coroutines, the main recommendation for background threads was to use an ExecutorService, for example:

execution took 1ms on average, compared to 110ms for coroutines. I found this held true for all theExecutors factory methods (they make it possible to use various types of thread pools). 🚀
As pointed out by Jake Wharton, the difference is partly due to ExecutorService being preloaded by the Zygote, a special part of the Android framework that shares code between processes. Other concurrency frameworks which, like coroutines, aren’t preloaded, will also have a comparatively high initialization cost.
That said, coroutines have a lot of advantages over ExecutorService. They’ve got scopes, suspending functions, they’re much more lightweight than threads, etc. The general recommendation to use them in Android applications is sound, but their impressive feature set has an initialization cost at the moment. Perhaps the Kotlin and Android teams will be able to optimize this in the future. Until then, best to avoid using coroutines in your Application class, or in your main Activity, if startup time is a primary concern.

この記事が気に入ったらサポートをしてみませんか?