Get ready to uncover some of the secrets of faster build times – an investment that’s not just about speed, but about creating a more satisfying and efficient development journey for you and your entire team.
Measuring and Improving Build Times
Measure the time of the builds
Alright, first of all, we need to understand that in order to measure concrete results, we need to do our best effort to use the same environment for the before / after tests. For example: use the same machine, connected to power, without running other applications.
Now, we need to think about which build do we want to improve:
Clean Builds
: Clean builds, also known as full builds, involve compiling the entire codebase from scratch, regardless of whether any changes have been made. To improve clean builds’ time:- Check the Improve Compile Time in Xcode Projects section
- Check the Improve Compile Time in SPM Packages section
- Check the Build with Timing Summary + Recent Build TimeLine section
Incremental Builds
: Incremental builds involve compiling only the code that has changed since the last build. To improve incremental builds’ time:- Use modules (SPM Packages work great)
- Use the correct Access Level in your code
- Check the Build with Timing Summary + Recent Build TimeLine section
Build with Timing Summary and Recent Build TimeLine:
Xcode provides two great tools to measure the compilation time:
1. Build with Timing Summary:
From the Product
Menu -> Perform Action -> Build With Timing Summary:
- Note: For clean builds, remember to clean the build folder (
Command + Shift + K
) before performing the build. - Once the build finishes, select it from the
Report navigator
, selectRecent
andAll Messages
sub tabs, then scroll all the way down to check the report.
Clean Build:
Incremental Build:
Note: This is a brand new project with only one file. There is test code inside that file to make the compilation slower, for the sake of this article.
As we can see, the Clean build is taking ~6 seconds
to compile and ~5 seconds
in a PhaseScriptExecution. Whereas the Incremental build only reports the same ~5 seconds
in the same PhaseScriptExecution.
So, if we take a look at why is the PhaseScriptExecution
taking too long, we can save up valuable time on each
build! That’s a lot of saved time over time.
You can do the math, if you have 5 developers, running 30 incremental builds a day, just saving up 2 seconds
per build will end up in 25 saved minutes
per week. And that’s without counting the CI time.
In this example, by cutting down all the extra work in the PhaseScriptExecution
, we’ve reduced the incremental build time by 5 seconds:
We’ll take a look at how to reduce the clean build time in the Improve Compile Time in Xcode Projects section.
2. Recent Build TimeLine
The Recent Build TimeLine
feature is quite handy for analyzing build times. To access it, select a recent build and go to Editor -> Assistant
. This timeline view provides a visual representation of build processes and times, helping you identify areas that need optimization.
Clean Build:
Note that each row represents a different core, on a bigger project, each core should be filled up with work in parallel, to make the build run faster. In this example, the project only needs to compile one file, that’s why what we see is only one core doing most of the work.
We can also see, that there are 2 main blocks of work:
- Compiling the ContentView.swift file
- The
PhaseScriptExecution
After the fix to the PhaseScriptExecution
(Incremental Build):
Improve Compile Time in Xcode Projects:
It’s time to roll up our sleeves and reduce the compilation time of our code.
The first step is to make Xcode display warnings in the code that takes too long to compile:
- Select your target project
- Go to the
Build Settings
tab. - Select the
All
sub tab. - Filter
Other Swift Flags
. - Add the following:
-Xfrontend -warn-long-function-bodies=<milliseconds>
-Xfrontend -warn-long-expression-type-checking=<milliseconds>
I usually start replacing the <milliseconds>
part with 50
.
These flags enable warnings for long function bodies and expression type checking, allowing you to identify potential bottlenecks in your codebase.
Example
Here is a method that takes too long to type-check:
We can reduce the build time by being more explicit with the types, and avoid using too many chained high order functions:
Now, we can check our build times again:
Clean Build:
Incremental Build:
Improve Compile Time in SPM Packages:
We can actually do the same For SPM packages, by applying the following swiftSettings
to the target:
.target(
name: "BuildTimesPackage",
dependencies: [],
swiftSettings: [
.unsafeFlags([
"-Xfrontend",
"-warn-long-function-bodies=50",
"-Xfrontend",
"-warn-long-expression-type-checking=50"
])
]
),
You could build your whole app target with multiple SPM packages using those settings, or just build a specific module as a standalone target with those settings.
Example
Note: I’ve added the exact same code that took ~5 seconds to compile in the main app target to the SPM Package.
Now we can perform a clean build and measure the time:
TimeLine:
Timing Summary:
Warnings:
🤯 This actually blew my mind.
The exact same code took ~1.3 seconds
to compile in a clean build in the SPM package.
That's almost 75% faster than the main app target,
just by moving it to a package.
I will have to do more research,
but it seems that the SPM compiler
is far more efficient than the one in Xcode.
We can also see that now the TimeLine
displays a more efficient use of the cores.
It’s also worth noticing, that when using different modules, the TimeLine
is way more useful than the Timing Summary
to identify the issues.
Fix:
After applying the same fix as above, we can make a new clean build and:
SwiftLint Rules:
So, is there a way to enforce the avoidance of type inference?
Yes, we can use a couple of SwiftLint rules:
The explicit_init and explicit_type_interface rules can indeed help streamline your code and potentially reduce build times. Ensuring clarity in your code’s initialization and type interfaces can prevent unnecessary ambiguity that might slow down the build process.
We could also add this custom rule, to avoid the usage of the .init
sugar syntax.
init_with_name:
name: "Init With Name"
message: "Prefer let object = Class() instead of let object: Class = .init()"
included: ".*.swift"
regex: '(?<!self|super)\.init\('
match_kinds:
- identifier
- keyword
severity: warning
Note: I'm not 100% sure if helping the compiler by
providing all the types, and avoiding the type inference
actually saves time for each method / property.
However, most of the times that I've seen methods taking
longer than 50ms to compile, providing the explicit types
seems to fix the warnings.
At the end of the day, I think opting in to these rules or not
should be a team decision, keeping an eye to the build time
and to the way they like to work with syntactic sugar.
Other Xcode optimizations
If you create a new Xcode project today, these settings will already be set correctly to optimize the build by default, but in case you are working with a legacy project (or a project where these settings were changed), here is the list of the build settings that are established as the better ones for improving the build times:
Swift Compiler:
Architectures:
Build Options:
Additional Tips:
- Check that your scheme’s build configuration is set to
Debug
. - Use a modularized architecture: Take advantage of how easy it’s to move code to modules using SPM.
- Avoid Type Inference: Explicitly defining types can prevent the compiler from spending extra time inferring types, leading to faster builds.
- Avoid shorthand enums usage. Instead of
status == .blocked
, usestatus == UserStatus.blocked
. Note: I couldn’t find a way to enforce this rule with a linter yet. - Use View Composition: Embracing view composition can result in more modular and focused code, which can lead to improved build times.
- Access Control: Employing the correct access control for your code can help the compiler optimize compilation, reducing unnecessary work.
- Use Periphery to find and delete unused code.
- Or read my Periphery guide.
- Watch the Demystify parallelization in Xcode builds
WWDC 2022
video. - Read the Build performance analysis for speeding up Xcode builds article from Antoine van der Lee
- Read the How to optimize Xcode project Build time article from Rushabh Singh
Let me know if you try some of the things explained in this article and if that helped to improve your build times. I’ll be happy to talk about it 😁.
Are there any other improvement tips that I missed and you are using in your projects?