You’re not alone if you’ve also struggled with sluggish Gradle builds in a multi-module project. Recently, we undertook the challenge of significantly reducing build times for a client with over 100 Kotlin Multiplatform modules, and achieved a more than 50% boost in speed across platforms. In this post, we’ll walk you through the steps we took. We hope it proves valuable to fellow developers facing similar challenges.
Benchmark Your Builds
Before diving into optimizations, it’s crucial to understand the baseline. Gather build times from all team members, ensuring the Gradle build cache is disabled. Use handy
--no-build-cache option to any Gradle task command to run without build cache. Unsurprisingly, in our case, the Intel-based mac machines were really slow compared to Apple chip ones and we didn’t have anyone with a Windows machine. This information is key for later comparison and improvement assessment.
Note: One thing we realized that, some optimizations below might be more applicable to legacy or tech-debt-laden projects.
Tools for Insight
Gradle Build Scan
Utilize the power of Gradle Build Scan to delve into the details of your builds. In my opinion, it’s not just a diagnostic tool; it’s a learning tool.
In our case, we uncovered insights into the sluggishness of
ios link tasks in the Kotlin Multiplatform setup. Comparing build scans on different CI machines helped identify the fastest configuration for a machine to use on our CI. Enabling parallel builds and analyzing the results through build scan reports further validated our improvements.
Android Studio Build Analyser
Android Studio provides a built-in analyser tool. It inspects build performance and provides warning around potential issues.
Using this, we noticed the lingering
android.enableJetifier=true usage due to outdated
Picasso library that didn’t use
AndroidX libraries. That might have hindered build speed too. Addressing this involved updating
Picasso and removing the
jetifier flag from
Through the Build Analyser, we also found unnecessary use of
multiDexEnabled in few modules along with some old Gradle configurations.
MultiDex was unnecessary with
minSdkVersion as 21.
We observed a significant number of modules relying on other modules via
api dependency. The problem with having
api dependencies is that Gradle would
recompile them when implementation details change. It’s because they appear on
compile classpaths. This can have significant
ripple of recompilations in a multi-module setup and affect the build times.
To mitigate this, we shifted to using
implementation for most dependencies, reserving
api only for those exported as part of
Enable Gradle Parallel Execution
In a multi-subproject setup, Gradle may benefit greatly from parallel execution. With the shift from
implementation, enabling parallel execution further slashed build times.
Mindful Use of External Gradle Plugins
Some Gradle plugins can be the culprits behind slower builds. Our project had a couple of Gradle plugins like that. They used to execute on every sync, every task and run through all the modules in the project.
Scrutinize third-party plugins before integration, especially in a multi-module setup. Avoid unnecessary global application using
allprojects blocks; apply plugins only where needed.
Unnecessary KMP targets
When managing Kotlin Multiplatform (KMP) projects, it’s crucial to evaluate the necessity of added KMP targets. In our experience, we found that inadvertently adding targets that aren’t required for the project can introduce inefficiencies.
For instance, we had inherited some modules with added
JVM targets for certain KMP modules from older test modules. Over time, more modules were introduced, and the JVM target persisted without any actual use. This led to unnecessary JVM-related build and test tasks executing during the build phase.
To address this, we recommend reviewing your KMP setup and removing any targets that don’t contribute to the project’s functionality. This not only streamlines the build process but also reduces unnecessary overhead, especially in a large multi-module project like ours.
Build only required XCFramework
In Kotlin Multiplatform (KMP) projects with iOS components, the iOS build task can be a significant contributor to extended build times. Often, KMP project script files include configurations for multiple iOS architectures using
iosSimulatorArm64() targets, or the shorthand
ios() that enables both
However, it’s essential to optimize this setup, especially considering that building for unnecessary architectures can consume substantial time and resources. For example, even if your local development machine has an
ARM architecture, building the
X64 framework unnecessarily adds to build times, and vice versa for other architectures.
Generally speaking, if things work on one architecture, then it’s high likely that it would work on other architectures too. So it’s fine if you at least keep building only one architecture locally. One approach is to introduce a boolean flag in your gradle.properties file, creating custom logic to enable a specific iOS target.
Are you on a team where not everybody calling the Kotlin code wants or needs to build it locally? We’ve built KMMBridge, a tool to help streamline the iOS dev flow in KMP.
Be Careful with Custom Gradle Tasks
While creating custom Gradle tasks might seem like a convenient way to handle common requirements, it’s crucial to exercise caution, especially in a multi-module setup. In our experience, some custom tasks had unintended consequences on build times.
For instance, we had custom tasks responsible for copying resources to another folder, but they ran across all modules, resulting in a noticeable slowdown during the build. To address this issue, we reconsidered our approach and opted for a different strategy to achieve the desired outcome without compromising build efficiency.
The key takeaway is to ensure that custom tasks are appropriately configured to avoid unnecessary repetition across modules or tasks
Keep build tools up-to-date
Always stay current with Android Studio, Android Gradle Plugin, and Gradle itself. Each iteration brings improvements, potentially enhancing build speed.
Upgrade to Android Gradle Plugin 8.0.0 for default behaviors that optimize builds (i.e. non-transitive R classes), especially for apps with multiple modules.
Newer build tools have better support for configuration cache. It seems like a very promising feature that can drastically improve build speeds in certain cases.
While configuration cache is a promising feature for faster builds, ensure compatibility with all your tools. As of Kotlin 1.9.10, the Kotlin Multiplatform plugin still lacks full support, limiting its effectiveness.
Kotlin 1.9.20 is supposed to being full support for configuration cache. We’re pretty pumped about it.
- Ensure Gradle Build Cache is enabled.
- Amount of code can be overwhelming in Gradle files with multi-module setup. Simplify Gradle scripts using the Gradle convention plugin to minimize duplicate configuration.
By following these steps, you may turbocharge your Gradle builds, regardless of the project size or complexity. Happy coding!