Optimize your build speed

Long build times slow down your development process. This page provides some techniques to help resolve build speed bottlenecks.

The general process of improving your app's build speed is as follows:

  1. Optimize your build configuration by taking a few steps that immediately benefit most Android Studio projects.
  2. Profile your build to identify and diagnose some of the trickier bottlenecks that may be specific to your project or workstation.

When developing your app, deploy to a device running Android 7.0 (API level 24) or higher whenever possible. Newer versions of the Android platform implement better mechanics for pushing updates to your app, such as the Android Runtime (ART) and native support for multiple DEX files.

Note: After your first clean build, you may notice that subsequent builds, both clean and incremental, perform much faster even without using any of the optimizations described on this page. This is because the Gradle daemon has a "warm-up" period of increasing performance—similar to other JVM processes.

Optimize your build configuration

Follow these tips to improve the build speed of your Android Studio project.

Keep your tools up to date

The Android tools receive build optimizations and new features with almost every update. Some tips on this page assume you're using the latest version. To take advantage of the latest optimizations, keep the following up to date:

Use KSP instead of kapt

The Kotlin Annotation Processing Tool (kapt) is significantly slower than the Kotlin Symbol Processor (KSP). If you are writing annotated Kotlin source and using tooling that processes annotations (such as Room) that supports KSP, you'll want to migrate to KSP.

Avoid compiling unnecessary resources

Avoid compiling and packaging resources that you aren't testing, such as additional language localizations and screen-density resources. Instead, only specify one language resource and screen density for your "dev" flavor, as shown in the following sample:

Groovy

android {
    ...
    productFlavors {
        dev {
            ...
            // The following configuration limits the "dev" flavor to using
            // English stringresources and xxhdpi screen-density resources.
            resourceConfigurations "en", "xxhdpi"
        }
        ...
    }
}

Kotlin

android {
    ...
    productFlavors {
        create("dev") {
            ...
            // The following configuration limits the "dev" flavor to using
            // English stringresources and xxhdpi screen-density resources.
            resourceConfigurations("en", "xxhdpi")
        }
        ...
    }
}

Experiment with putting the Gradle Plugin Portal last

In Android, all plugins are found in the google() and mavenCentral() repositories. However, your build might need third-party plugins that are resolved using the gradlePluginPortal() service.

Gradle searches repositories in the order that they're declared, so build performance is improved if the repositories listed first contain most of the plugins. Therefore, experiment with the gradlePluginPortal() entry by putting it last in the repository block in your settings.gradle file. In most cases, this minimizes the number of redundant plugin searches and improves your build speed.

For more information about how Gradle navigates multiple repositories, see Declaring multiple repositories in the Gradle documentation.

Use static build config values with your debug build

Always use static values for properties that go in the manifest file or resource files for your debug build type.

Using dynamic version codes, version names, resources, or any other build logic that changes the manifest file requires a full app build every time you want to run a change, even if the actual change might otherwise require only a hot swap. If your build configuration requires such dynamic properties, then isolate those properties to your release build variants and keep the values static for your debug builds, as shown in the following sample:

  ...
  // Use a filter to apply onVariants() to a subset of the variants.
  onVariants(selector().withBuildType("release")) { variant ->
      // Because an app module can have multiple outputs when using multi-APK, versionCode
      // is only available on the variant output.
      // Gather the output when we are in single mode and there is no multi-APK.
      val mainOutput = variant.outputs.single { it.outputType == OutputType.SINGLE }

      // Create the version code generating task.
      val versionCodeTask = project.tasks.register("computeVersionCodeFor${variant.name}", VersionCodeTask::class.java) {
          it.outputFile.set(project.layout.buildDirectory.file("versionCode${variant.name}.txt"))
      }

      // Wire the version code from the task output.
      // map will create a lazy Provider that:
      // 1. Runs just before the consumer(s), ensuring that the producer (VersionCodeTask) has run
      //    and therefore the file is created.
      // 2. Contains task dependency information so that the consumer(s) run after the producer.
      mainOutput.versionCode.set(versionCodeTask.flatMap { it.outputFile.map { it.asFile.readText().toInt() } })
  }
  ...

  abstract class VersionCodeTask : DefaultTask() {

    @get:OutputFile
    abstract val outputFile: RegularFileProperty

    @TaskAction
    fun action() {
        outputFile.get().asFile.writeText("1.1.1")
    }
  }

See the setVersionsFromTask recipe on GitHub to learn how to set a dynamic version code in your project.

Use static dependency versions

When you declare dependencies in your build.gradle files, avoid using dynamic version numbers (those with a plus sign at the end, such as 'com.android.tools.build:gradle:2.+'). Using dynamic version numbers can cause unexpected version updates, difficulty resolving version differences, and slower builds caused by Gradle checking for updates. Use static version numbers instead.

Create library modules

Look for code in your app that you can convert into an Android library module. Modularizing your code this way allows the build system to compile only the modules you modify and cache those outputs for future builds. Modularization also makes parallel project execution more effective when you enable that optimization.

Create tasks for custom build logic

After you create a build profile, if the build profile shows that a relatively long portion of the build time is spent in the **Configuring Projects** phase, review your build.gradle scripts and look for code to include in a custom Gradle task. By moving some build logic into a task, you help ensure that the task runs only when required, results can be cached for subsequent builds, and that build logic becomes eligible to run in parallel if you enable parallel project execution. To learn more about taks for custom build logic, read the official Gradle documentation.

Tip: If your build includes a large number of custom tasks, you might want to declutter your build.gradle files by creating custom task classes. Add your classes to the project-root/buildSrc/src/main/groovy/ directory; Gradle automatically includes those classes in the classpath for all build.gradle files in your project.

Convert images to WebP

WebP is an image file format that provides lossy compression (like JPEG) as well as transparency (like PNG). WebP can provide better compression than either JPEG or PNG.

Reducing image file sizes without having to perform build-time compression can speed up your builds, especially if your app uses a lot of image resources. However, you may notice a small increase in device CPU usage while decompressing WebP images. Use Android Studio to easily convert your images to WebP.

Disable PNG crunching

If you don't convert your PNG images to WebP, you can still speed up your build by disabling automatic image compression every time you build your app.

If you're using Android Gradle plugin 3.0.0 or higher, PNG crunching is disabled by default for the "debug" build type. To disable this optimization for other build types, add the following to your build.gradle file:

Groovy

android {
    buildTypes {
        release {
            // Disables PNG crunching for the "release" build type.
            crunchPngs false
        }
    }
}

Kotlin

android {
    buildTypes {
        getByName("release") {
            // Disables PNG crunching for the "release" build type.
            isCrunchPngs = false
        }
    }
}

Because build types or product flavors don't define this property, you need to manually set this property to true when building the release version of your app.

Experiment with the JVM parallel garbage collector

Build performance can be improved by configuring the optimal JVM garbage collector used by Gradle. While JDK 8 is configured to use the parallel garbage collector by default, JDK 9 and higher are configured to use the G1 garbage collector.

To potentially improve build performance, we recommend testing your Gradle builds with the parallel garbage collector. In gradle.properties set the following:

org.gradle.jvmargs=-XX:+UseParallelGC

If there are other options already set in this field, add a new option:

org.gradle.jvmargs=-Xmx1536m -XX:+UseParallelGC

To measure build speed with different configurations, see Profile your build.

Increase the JVM heap size

If you observe slow builds, and in particular the garbage collection takes more than 15% of the build time in your Build Analyzer results, then you should increase the Java Virtual Machine (JVM) heap size. In the gradle.properties file, set the limit to 4, 6, or 8 gigabytes as shown in the following example:

org.gradle.jvmargs=-Xmx6g

Then test for build speed improvement. The easiest way to determine the optimal heap size is to increase the limit by a small amount and then test for sufficient build speed improvement.

If you also use the JVM parallel garbage collector, then the entire line should look like:

org.gradle.jvmargs=-Xmx6g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -XX:+UseParallelGC -XX:MaxMetaspaceSize=1g

You can analyze the JVM memory errors by turning the HeapDumpOnOutOfMemoryError flag on. By doing so, the JVM will generate a heap dump, when running out of memory.

Use non-transitive R classes

Use non-transitive R classes to have faster builds for apps with multiple modules. Doing so helps prevent resource duplication by ensuring that each module's R class only contains references to its own resources without pulling references from its dependencies. This leads to faster builds and the corresponding benefits of compilation avoidance. This is the default behavior in Android Gradle plugin 8.0.0 and higher.

Starting with Android Studio Bumblebee, non-transitive R classes are on by default for new projects. For projects created with earlier versions of Android Studio, update them to use non-transitive R classes by going to Refactor > Migrate to Non-Transitive R Classes.

To learn more about app resources and the R class, see App resources overview.

Use non-constant R classes

Use non-constant R class fields in apps and tests to improve the incrementality of Java compilation and allow for more precise resource shrinking. R class fields are always not constant for libraries, as the resources are numbered when packaging the APK for the app or test that depends on that library. This is the default behavior in Android Gradle Plugin 8.0.0 and higher.

Disable the Jetifier flag

Since most projects use AndroidX libraries directly, you can remove the Jetifier flag for better build performance. To remove the Jetifier flag, set android.enableJetifier=false in your gradle.properties file.

The Build Analyzer can perform a check to see whether the flag can be safely removed to enable your project to have better build performance and migrate away from the unmaintained Android Support libraries. To learn more about the Build Analyzer, see Troubleshoot build performance.

Use the configuration cache

The configuration cache lets Gradle record information about the build tasks graph and reuse it in subsequent builds, so Gradle doesn't have to reconfigure the whole build again.

To enable the configuration cache, follow these steps:

  1. Check that all project plugins are compatible.

    Use the Build Analyzer to check whether your project is compatible with the configuration cache. The Build Analyzer runs a sequence of test builds to determine whether the feature can be turned on for the project. See issue #13490 for a list of plugins that are supported.

  2. Add the following code to the gradle.properties file:

      org.gradle.configuration-cache=true
      # Use this flag carefully, in case some of the plugins are not fully compatible.
      org.gradle.configuration-cache.problems=warn

When the configuration cache is enabled, the first time you run your project the build output says Calculating task graph as no configuration cache is available for tasks. During subsequent runs, the build output says Reusing configuration cache.

To learn more about the configuration cache, see the blog post Configuration caching deep dive and the Gradle documentation about the configuration cache.

Configuration cache issues introduced in Gradle 8.1 and Android Gradle Plugin 8.1

The configuration cache became stable in Gradle 8.1, and introduced file API tracking. Calls such as File.exists(), File.isDirectory() and File.list() are recorded by Gradle to track configuration input files.

Android Gradle Plugin (AGP) 8.1 uses these File APIs for some files that Gradle should not consider to be cache inputs. This triggers additional cache invalidation when used with Gradle 8.1 and higher, slowing build performance. The following are treated as cache inputs in AGP 8.1:

Input Issue Tracker Fixed in
$GRADLE_USER_HOME/android/FakeDependency.jar Issue #289232054 AGP 8.2
cmake output Issue #287676077 AGP 8.2
$GRADLE_USER_HOME/.android/analytics.settings Issue #278767328 AGP 8.3

If you use these APIs or a plugin that uses these APIs, you might experience a regression in your build time, because some build logic using these APIs can trigger additional cache invalidation. Please see Improvements in the build configuration input tracking for a discussion of these patterns and how to fix the build logic, or temporarily disable the file API tracking.