Build performance is critical to your productivity. The longer the build takes to complete, the more likely you’ll be taken out of your development flow. On top of that, since you run the build many times a day, even small periods of waiting can add up to significant disruption. The same is true for CI builds: The faster they are, the faster you can react to new issues and the more capacity you will have to do innovative experiments.

All this means that it’s worth investing some time and effort into making your build as fast as possible. This guide offers several avenues you can explore to make a build faster, along with plenty of detail on what sorts of things can degrade the performance of a build and why. Let’s start with some quick wins.

Easy improvements

A guide on performance tuning would normally start with profiling and something about premature optimisation being the root of all evil. Profiling is definitely important and we discuss it later in the guide, but there are some things you can do that will impact all your builds for the better at the flick of a switch.

Use latest Gradle and JVM versions

The Gradle team works continuously on improving the performance of different aspects of Gradle builds. If you’re using an old version of Gradle, you’re missing out on the benefits of that work. Always keep up with Gradle version upgrades. Doing so is low risk because the Gradle team ensures backwards compatibility between minor versions of Gradle. Staying up-to-date also makes transitioning to the next major version easier, because you’ll get early deprecation warnings.

Going up a major version is often just as easy. Only formerly deprecated APIs and behavior become failures at these boundaries. So be sure to fix those deprecation warnings! If they are caused by a third party plugin, let the author know right away, so you are not blocked when the next major release comes out.

As Gradle runs on the JVM, improvements in the performance of the latter will often benefit Gradle. Hence, you should consider running Gradle with the latest major version of the JVM.

Parallel project execution

Most builds consist of more than one project and some of those projects are usually independent of one another. Yet Gradle will only run one task at a time by default, regardless of the project structure (this will be improved soon). By using the --parallel switch, you can force Gradle to execute tasks in parallel as long as those tasks are in different projects.

You could see big improvements in build times as soon as you enable parallel builds. The extent of those improvements depends on your project structure and how many dependencies you have between them. A build whose execution time is dominated by a single project won’t benefit much at all, for example. Or one that has lots of inter-project dependencies resulting in few tasks that can be executed in parallel. But most multi-project builds should see a worthwhile boost to build times.

Parallel builds require projects to be decoupled at execution time, i.e. tasks in different projects must not modify shared state. Read more about that topic in the User Manual before using --parallel extensively. Also be aware that Gradle versions before 4.0 could run clean and build tasks in parallel, resulting in failures. On these older versions it is best to call clean separately.

You can also make building in parallel the default for a project by adding the following setting to the project’s gradle.properties file:

gradle.properties
org.gradle.parallel=true

Configure on demand

Imagine you have 300 subprojects all managed by a single Gradle build. Even if the configuration time for each project is just 50ms, that means every build will take at least 15s! That’s why it’s particularly crucial to keep a close eye on configuration times for projects that have a lot of modules.

With that in mind, wouldn’t it be better if Gradle only configured the projects that were actually required to execute the requested task(s)? Of course, and that’s where the --configure-on-demand command line option comes in. It avoids a lot of unnecessary configuration at the cost of working out which projects are required. We wouldn’t recommend using it if you only have a small number of quick-to-configure subprojects, but otherwise you should try it.

Configure on demand will only work reliably when you have decoupled projects. You can learn more about decoupled projects in the User Manual. Most importantly, using subprojects {} or allprojects {} blocks will mostly negate the benefit of configure-on-demand, because those blocks are executed eagerly.

You can make configure on demand the default for a project by adding the following setting to its gradle.properties file:

gradle.properties
org.gradle.configureondemand=true

Enable the daemon on old Gradle versions

The Gradle daemon is a mechanism for improving the performance of Gradle. As of Gradle 3.0, the daemon is enabled by default, but if you are using an older version, you should definitely enable it on local developer machines. You will see big improvements in build speed by doing so. You can learn how to do that in the 2.14 user guide.

On CI machines the benefit you can expect from the daemon depends on your setup. If you have long-lived CI agents and you build lots of small projects that all use the same Gradle version and JVM arguments, then the daemon can reduce your turnarounds. If your projects are big or more diverse, you probably won’t see much benefit. Generally it is safe to leave the daemon on, as Gradle 3.0 introduced health monitoring which will shut daemons down on memory pressure.

Adjust the daemon’s heap size

By default Gradle will reserve 1GB of heap space for your build, which is plenty for most projects, especially if you follow our advice on forked compilation further down in this guide. However, some very large builds might need more memory to hold Gradle’s model and caches. If this is the case for you, you can check in this larger memory requirement in your gradle.properties file:

gradle.properties
org.gradle.jvmargs=-Xmx2048M

That’s the end of our quick wins. From here on out, improving your build performance will require some elbow grease. We start with perhaps the most important step: finding out which bits of your build are slow and why.

High level profiling

As with any attempt to optimize, your first instinct should be to profile the system you’re interested in. Gradle provides two mechanisms for doing this and both of them give you reports that you can view in a browser. Let’s take a look at these two mechanisms - build scans and profile reports - before then discussing how to interpret the information provided in the reports.

Using Gradle build scans

Build scans are a feature provided by Gradle Inc. that aggregate information across multiple build runs and make those diagnostics available to you online as a report. You can

  • Easily share scans between developers and build masters

  • Track build performance over time

  • More easily identify the source of a problem

  • Identify issues specific to a single developer site

Gradle won’t generate build scans automatically, but you can easily enable them by following these steps:

  1. Apply the build-scan plugin as described on Gradle.com

  2. Run your builds with the --scan option, e.g. gradle build --scan

At the end of the build, Gradle displays the URL where your build scan awaits your attention.

The build scans themselves provide a lot of information, but the main area of interest in the early stages of diagnosis is the performance page. To get there, follow the link highlighted in the following screenshot of the build scan home page:

build scan home
Figure 1. Performance page link on build scan home page

The performance page gives you a breakdown of how long different stages of your build took to complete. As you can see from the following screenshot, you get to see how long Gradle took to start up, configure the build’s projects, resolve dependencies, and execute the tasks. You also get details about environmental properties, such as whether a daemon was used or not.

build scan performance page
Figure 2. Build scan performance page

We will look into the different categories presented in the report shortly. You can also learn more about build scans at Gradle.com.

Profile report

If you don’t have internet access or have some other reason not to use build scans, you can use the --profile command-line option:

$ gradle --profile <tasks>

This will result in the generation of an HTML report that you can find in the build/reports/profile directory of the root project. Each profile report has a timestamp in its name to avoid overwriting existing ones.

The report displays a breakdown of the time taken to run the build, though less detailed than a build scan. Here’s a screenshot of a real profile report showing the different categories that Gradle uses:

Sample Gradle profile report
Figure 3. An example profile report

Each of the main categories - Configuration, Dependency Resolution, and Task Execution - may reveal different time sinks that you may want to tackle. We’ll go through those categories in later sections, detailing the types of issue you may encounter for each one. Before then, let’s take a look at some of the items in the summary.

Understanding the profile report categories

Both build scans and the local profile reports break build execution down into the same categories. We’ll now look at those categories, what they mean, and what sorts of problems you can identify with them.

Startup

This reflects Gradle’s initialization time, which consists mostly of

  • JVM initialization and class loading

  • Downloading the Gradle distribution if you’re using the wrapper

  • Starting the daemon if a suitable one isn’t already running

  • Time spent executing any Gradle initialization scripts

Even if a build execution has a long startup time, a subsequent run will usually see a dramatic drop off in the startup time. The main reason for a build’s startup time to be persistently slow is a problem in your init scripts. Double check that the work you’re doing there is necessary and as performant as possible.

Settings and buildSrc

Soon after Gradle has got itself up and running, it initializes your project. This commonly just means processing your settings.gradle file, but if you have custom build logic in a buildSrc directory, that gets built as well.

The sample profile report shows a time of just over 8 seconds for this category, the vast majority of which was spent building the buildSrc project. This part fortunately won’t take so long once buildSrc is built once as Gradle will consider it up to date. The up-to-date checks still take a little time, but nowhere near as much. If you do have problems with a persistently time consuming buildSrc phase, you should consider breaking it out into a separate project whose JAR artifact is added to the build’s classpath.

The settings.gradle file rarely has computationally or IO expensive code in it. If you find that Gradle is taking a significant amount of time to process it, you should use more traditional profiling methods, such as the Gradle Profiler to determine why.

Loading projects

It normally doesn’t take a significant amount of time to load projects, nor do you have any control over it. The time spent here is basically a function of the number of projects you have in your build.

The rest of the summary relates to the main categories, which we cover in detail in the next sections.

Configuration

As the user guide describes in the build lifecycle chapter, a Gradle build goes through three phases: initialization, configuration, and execution. The important thing to understand here is that configuration code always executes regardless of which tasks will run. That means any expensive work performed during configuration will slow every invocation down, even simple ones like gradle help and gradle tasks.

The profile report will help you identify which projects take the most time to configure, but that’s all. The next few subsections introduce techniques that can help improve the configuration time and explain why they work.

Apply plugins judiciously

Every plugin that you apply to a project adds to the overall configuration time. Some plugins have a greater impact than others. That doesn’t mean you should avoid using plugins, but you should take care to only apply them where they’re needed. For example, it’s easy to apply plugins to all projects via allprojects {} or subprojects {} even if not every project needs them.

Ideally, plugins should not incur a significant configuration-time cost. If they do, the focus should be on improving the plugin. Nonetheless, in projects with many modules and a significant configuration time, you should spend a little time identifying any plugins that have a notable impact. The only reliable way to do this is by running a build twice: once with the plugin applied and once without.

Avoid expensive or blocking work

This is fairly obvious based on what we’ve already said about the configuration phase, but it’s not hard to accidentally break this rule. It’s usually clear when you’re encrypting stuff or calling remote services during configuration if that code is in a build file. But logic like this is more often found in plugins and occasionally custom task classes. Any expensive work in a plugin’s apply() method or a tasks’s constructor should be a red flag. The most common and less obvious mistake is resolving dependencies at configuration time, which we cover in its own chapter further below.

Statically compile tasks and plugins

Plugins and occasionally tasks perform work during the configuration phase. These are often written in Groovy for its concise syntax, API extensions to the JDK, and functional methods using closures. However, it’s important to bear in mind that there is a small cost associated with method calls in dynamic Groovy. When you have lots of method calls repeated across lots of projects, the cost can add up.

In general, we recommend that you use either @CompileStatic on your Groovy classes (where possible) or write those classes in a statically compiled language, such as Java. This only really applies to large projects or plugins that you publish publicly (because they may be applied to large projects by other users). If you do need dynamic Groovy at any point, simply use @CompileDynamic for the relevant methods.

Note The DSL you’re used to in the build script relies heavily on Groovy’s dynamic features, so if you want to use static compilation in your plugins, you will have to switch to more traditional Java-like syntax. For example, to create a new copy task, you would use code like this:

build.gradle
project.tasks.create("copyFiles", Copy) { Task t ->
    t.into "${project.buildDir}/output"
    t.from project.configurations.getByName("compile")
}

You can see how this example uses the create() and getByName() methods, which are available on all Gradle “domain object containers”, like tasks, configurations, dependencies, extensions, etc. Some collections have dedicated types, TaskContainer being one of them, that have useful extra methods like the create() method above that takes a task type.

If you do decide to use static compilation, we recommend using an IDE as it will quickly show errors due to unrecognised types, properties, and methods. You’ll also get auto-completion, which is always handy.

Dependency resolution

Software projects rely on dependency resolution to simplify the integration of third-party libraries and other dependencies into the build. This does come at a cost as Gradle has to contact remote servers to find out about said dependencies and download them where necessary. Advanced caching helps speed things up tremendously, but you still need to watch out for a few pitfalls that we discuss next.

Dynamic and snapshot versions

Dynamic versions, such as “2.+”, and snapshot (or changing) versions force Gradle to contact the remote repository to find out whether there’s a new version or snapshot available. By default, Gradle will only perform the check once every 24 hours, but this can be changed. Look out for cacheDynamicVersionsFor and cacheChangingModulesFor in your build files and initialization scripts in case they are set to very short periods or disabled completely. Otherwise you may be condemning your build users to frequent slower-than-normal builds rather than a single slower-than-normal build a day.

You may be able to use fixed versions - like 1.2 and 3.0.3.GA - in which case Gradle will always use the cached version. But if you want or need to use dynamic and snapshot versions, make sure you tune the cache settings according to your requirements.

Don’t resolve dependencies at configuration time

Dependency resolution is an expensive process, both in terms of IO and computation. Gradle reduces - and eliminates in some cases - the required network traffic through judicious caching, but there is still work it needs to do. Why is this important? Because if you trigger dependency resolution during the configuration phase, you’re going to add a penalty to every build that runs.

The key question to answer is what triggers dependency resolution? The most common cause is the evaluation of the files that make up a configuration. This is normally a job for tasks, since you typically don’t need the files until you’re ready to do something with them in a task action. However, imagine you’re doing some debugging and want to display the files that make up a configuration through judicious caching. One way you can do this is by injecting a print statement:

build.gradle
task copyFiles(type: Copy) {
    println ">> Compilation deps: ${configurations.compile.files}"
    into "$buildDir/output"
    from configurations.compile
}

The files property will force Gradle to resolve the dependencies, and in this example that’s happening during the configuration phase. Now every time you run the build, no matter what tasks you execute, you’ll take a hit from the dependency resolution on that configuration. It would be better to add this in a doFirst() action.

build.gradle
task copyFiles(type: Copy) {
    into "$buildDir/output"
    from configurations.compile
    doFirst {
        println ">> Compilation deps: ${configurations.compile.files}"
    }
}

Note that the from() declaration doesn’t resolve the dependencies because you’re using the dependency configuration itself as an argument, not its files. The Copy task handles the resolution of the configuration itself during task execution, which is exactly what you want.

The performance page of build scans explicitly shows how dependency resolution time is split across project configuration and task execution, so it’s easy to identify this particular issue. If you’re using the older profile reports, a simple way to determine whether you’re resolving dependencies during configuration is to run

$ gradle --profile help

and look at the time spent on dependency resolution. This should be zero, so if it’s not, you’re resolving dependencies at configuration time. The report will also tell you which configurations are being resolved, which should help in diagnosing the source of the configuration-time resolution.

Avoid unnecessary and unused dependencies

You will sometimes encounter situations in which you’re only using one or two methods or classes from a third-party library. When that happens, you should seriously consider implementing the required code yourself in the project or copying it from an open source library if that’s an option for you. Remember that managing third-party libraries and their transitive dependencies adds a not insignificant cost to project maintenance as well as build times.

Another thing to watch out for is the existence of unused dependencies. This can easily happen after code refactoring when a third-party library stops being used but isn’t removed from the dependency list. You can use the Gradle Lint plugin to identify such dependencies.

Minimize repository count

When Gradle attempts to resolve a dependency, it searches through each repository in the order that they are declared until it finds that dependency. This generally means that you want to declare the repository hosting the largest number of your dependencies first so that only that repository is searched in the majority of cases. You should also limit the number of declared repositories to the minimum viable number for your build to work.

One technique available if you’re using a custom repository server is to create a virtual repository that aggregates several real repositories together. You can then add just that repository to your build file, further reducing the number of HTTP requests that Gradle sends during dependency resolution.

Be careful with custom dependency resolution logic

Dependency resolution is a hard problem to solve and making it perform well simply adds to the challenge. And yet, Gradle still needs to allow users to model dependency resolution in the way that best suits them. That’s why it has a powerful API for customizing how the dependency resolution works.

Simple customizations — such as forcing specific versions of a dependency or substituting one dependency for another — don’t have a big impact on dependency resolution times. But if custom logic involves downloading and parsing extra POMs, for example, then the impact can be significant.

You should use build scans or profile reports to check that any custom dependency resolution logic you have in your build doesn’t adversely affect dependency resolution times in a big way. And note that this could be custom logic you have written yourself or it could be part of a plugin that you’re using.

Task execution

The fastest task is one that doesn’t execute. If you can find ways to skip tasks you don’t need to run, you’ll end up with a faster build overall. In this section, we’ll discuss a few ways to achieve task avoidance in Gradle.

Different people, different builds

It seems to be very common to treat a build as an all or nothing package. Every user has to learn the same set of tasks that have been defined by the build. In many cases this makes no sense. Imagine you have both front-end and back-end developers: do they want the same things from the build? Of course not, particularly if one side is HTML, CSS and Javascript, while the other is Java and servlets.

It’s important that a single task graph underpins the build to ensure consistency. But you don’t need to expose the entire task graph to everyone. Instead, think in terms of sets of tasks forming a restricted view upon the task graph, with each view designed for a specific group of users. Do front-end developers need to run the server side unit tests? No, so it would make no sense to force the cost of running the tests on those users.

With that in mind, consider the different workflows that each distinct group of users require and try to ensure that they have the appropriate “view” with no unnecessary tasks executed. Gradle has several ways to aid you in such an endeavour:

  • Assign tasks to appropriate groups

  • Create useful aggregate tasks (ones that have no action and simply depend on a set of other tasks, like assemble)

  • Defer configuration via gradle.taskGraph.whenReady() and others, so you can perform verification only when it’s necessary

It definitely requires some effort and an investment in time to craft suitable build views, but think about how often users run the build. Surely that investment is worth it if it saves users time on a daily basis?

Incremental build

You can can avoid executing tasks, even if they’re required by a user. If neither a task’s inputs nor its output have changed since the last time it was run, Gradle will not run it again.

Incremental build is the name Gradle gives to this feature of checking inputs and outputs to determine whether a task needs to run again or not. Most tasks provided by Gradle take part in incremental build because they have been defined that way. You can also make your own tasks integrate with incremental build, as described in the user guide. The basic idea is to mark the task’s properties that have an impact on whether a task needs to run. You can learn more in the user guide.

You can easily identify good candidates for incremental build or tasks that aren’t up to date when they should be by looking at the timeline view in a build scan. The tasks are sorted by longest duration first, making it easy to pick out the slowest tasks. Pick the slowest of your custom tasks and make it incremental, then measure again and repeat.

Caching task outputs

Incremental build works locally, based on the previous execution of a task. Gradle can also store task outputs in a build cache, and retrieve them later when the same task with the same inputs is about to be executed. You can use a local cache to reuse task outputs on your computer. This helps reduce build times when switching branches.

It is also possible to use a shared build cache service, like the one provided by Gradle Enterprise. Shared caches can reduce the number of tasks you need to execute by reusing outputs already generated elsewhere. This can significantly decrease build times for both CI and developer builds.

We have a complete guide about using the build cache. It covers the different scenarios caching can improve, and detailed discussions of the different caveats you need to be aware of when enabling caching for a build.

Partial builds

Incremental build definitely improves build times, but you need to remember that the up-to-date checks still take time. This has important implications for multi-project builds that have a large number of subprojects. If the task you want to execute ultimately depends on the execution of twenty other subprojects, you have to wait until the build has finished checking those before it gets round to your task. Some of them may even have non-incremental tasks that end up running, even if nothing has changed.

Gradle offers a nice shortcut if you know that a task’s project dependencies haven’t changed: use the -a command line option. This forces Gradle to effectively ignore all the dependent projects and only execute the required tasks that are defined in the target project. Project dependencies will still be included on the appropriate classpaths, so the project will build as before. Just be sure there haven’t been any changes to the projects the target depends on!

Gradle also supports other forms of partial build via the base plugin, which adds the following tasks:

  • buildNeeded - will execute the build task in the target project and all those projects it depends on. This verifies that the projects you depend on are working correctly. If that’s not the case, they may break the target project’s tests or some other part of the build.

  • buildDependents - will execute the build task in the target project and all projects that depend on it. This checks that you haven’t broken those projects after making some changes.

These tasks are slower than just running build in the target project as they do more work, but they are an effective alternative to running gradle build, which runs build in all the projects of a multi-project build.

Suggestions for Java projects

The following suggestions are specific to projects using the java plugin or one of the other JVM languages.

Running tests

A significant proportion of the build time for many projects consists of the test tasks that run. These could be a mixture of unit and integration tests, with the latter often being significantly slower. Gradle has a few ways to help your tests complete faster:

  • Parallel test execution

  • Process forking options

  • Disable report generation

Let’s look at each of these in turn.

Parallel test execution

Gradle will happily run multiple test cases in parallel, which is useful when you have several CPU cores and don’t want to waste most of them. To enable this feature, just use the following configuration setting on the relevant Test task(s):

build.gradle
test.maxParallelForks = 4

The normal approach is to use some number less than or equal to the number of CPU cores you have. We recommend you use the following algorithm by default:

build.gradle
test.maxParallelForks = Runtime.runtime.availableProcessors().intdiv(2) ?: 1

Note that if you do run the tests in parallel, you will have to ensure that they are independent, i.e. don’t share resources, be that files, databases or something else. Otherwise there is a chance that the tests will interfere with each other in random and unpredictable ways.

Forking options

Gradle will run all tests in a single forked VM by default. This can be problematic if there are a lot of tests or some very memory-hungry ones. One option is to run the tests with a big heap, but you will still be limited by system memory and might encounter heavy garbage collection that slows the tests down.

Another option is to fork a new test VM after a certain number of tests have run. You can do this with the forkEvery setting:

build.gradle
test.forkEvery = 100

Just be aware that forking a VM is a relatively expensive operation, so a small value here will severely handicap the performance of your tests.

Report generation

Gradle will automatically create test reports by default regardless of whether you want to look at them. That report generation takes time, slowing down the overall build. Reports are definitely useful, but do you need them every time you run the build? Perhaps you only care if the tests succeed or not.

To disable the test reports, simply add this configuration:

build.gradle
test {
    reports.html.enabled = false
    reports.junitXml.enabled = false
}

This example applies to the default Test task added by the Java plugin, but you can also apply the configuration to any other Test tasks you have.

One thing to bear in mind is that you will probably want to conditionally disable or enable the reports, otherwise you will have to edit the build file just to see them. For example, you could enable the reports based on a project property:

build.gradle
test {
    if (!project.hasProperty("createReports")) {
        reports...
    }
}

Compiling Java

The Java compiler is quite fast, especially compared to other languages on the JVM. And yet, if you’re compiling hundreds of non-trivial Java classes, even a short compilation time adds up to something significant. You can of course upgrade your hardware to make compilation go faster, but that can be an expensive solution. Gradle offers a couple of software-based solutions that might be more to your liking:

  • Compiler daemon

  • Compile avoidance and the java-library plugin

  • Incremental compilation

Compiler daemon

The Gradle Java plugin allows you to run the compiler as a separate process by using the following configuration for any JavaCompile task:

<taskname>.options.fork = true

or, more commonly, to apply the configuration to all Java compilation tasks:

build.gradle
tasks.withType(JavaCompile) {
    options.fork = true
}

This process is reused for the duration of a build, so the forking overhead is minimal. The benefit of forking is that the memory-intensive compilation happens in a different process, leading to much less garbage collection in the main Gradle daemon. Less garbage collection in the daemon means that Gradle’s infrastructure can run faster, especially if you are also using --parallel.

It’s unlikely to be useful for small projects, but you should definitely consider it if a single task is compiling close to a thousand or more source files together.

Compile avoidance

A lot of the time, you are only changing internal implementation details of your code, e.g. editing a method body. Starting with Gradle 3.4, these so-called ABI-compatible changes no longer trigger recompilation of downstream projects. This especially improves build times in large multi-project builds with deep dependency chains.

Note: If you use annotation processors, you need to explicitly declare them in order for compile avoidance to work. Read more about compile avoidance in the user guide.

The java-library plugin

For a long time, you would declare your compile time dependencies using the compile configuration and all of them would be leaked into downstream projects. Since Gradle 3.4, you can now clearly separate which dependencies are part of your api and which are only implementation details. Implementation dependencies are not leaked into the compile classpath of downstream projects, which means that they will no longer be recompiled when such an implementation detail changes.

dependencies {
   api project("myUtils")
   implementation "com.google.guava:guava:21.0"
}

This can significantly reduce the "ripple" effect of a single change in large multi-project builds. The implementation Configuration is available in the java plugin. api dependencies can only be defined by libraries, which should use the new java-library plugin.

Incremental compilation

Gradle can analyze dependencies down to the individual class level in order to recompile only the classes that were affected by a change. This option will soon become the default setting, but you can already use it today to significantly speed up your incremental build times:

tasks.withType(JavaCompile) {
    options.incremental = true
}

Low level profiling

Sometimes your build can be slow even though your build scripts are doing everything right. This often comes down to inefficiencies in plugins and custom tasks or constrained resources. The best way to find these kinds of bottlenecks is using the Gradle Profiler. The Gradle Profiler allows you to define scenarios like "Running 'assemble' after making an ABI-breaking change" and then automatically runs your build several times to warm it up and collect profiling data. It can be used to produce build scans or together with other major profilers like JProfiler and YourKit. Using these method-level profilers can often help you find ineffcient algorithms in custom plugins. If you find that something in Gradle itself is slowing down your build, don’t hesitate to send us a profiler snapshot at performance@gradle.com.

Suggestions for Android builds

Everything we have talked about so far applies to Android builds too, since they’re based on Gradle. Yet Android also introduces its own performance factors. The Android Studio team has put together their own excellent performance guide. You can also watch the accompanying talk from Google IO 2017.

Summary

Performance is a feature and the Gradle team are always attempting to make the Gradle defaults as fast as possible because they know that their users' time is valuable. Even so, Gradle supports a huge variety of builds, which means that the defaults won’t always be ideal for your project. That’s why we introduced you to some settings and task options that allow you to tweak the behavior of the build in your favor. You should also familiarise yourself with any other available options on your long running tasks and with the generic Gradle build environment settings.

Beyond those settings, remember that the two big contributors to build times are configuration and task execution, although the base cost of the former drops with almost every major Gradle release. And as far as the configuration phase goes, you should now have a good idea of the pitfalls you need to avoid.

You have more control over task execution, since you can avoid running tasks or running them too often, and you can also code your own tasks to be as performant as possible. In the future, Gradle will offer more features to help with execution performance. Things like parallel task execution. You have plenty to look forward to!

In the meantime, we hope the ideas in this guide help you cut your build times and improve the overall user experience.

Help improve this guide

Have feedback or a question? Found a typo? Like all Gradle guides, help is just a GitHub issue away. Please add an issue or pull request to gradle-guides/performance and we’ll get back to you.