Xcode Build Time Optimization - Part 2


April 8, 2020

#xcode #workflow #optimization

The build time speedup is crucial for developers productivity and the product’s time to market. It is quite important to keep you build time under control and improve one if needed.

In the previous article we’ve learned how to measure the Xcode build time and get some metrics to analyze. In this blog post, I’ll show you how we can resolve the Xcode build time bottlenecks and speed it up.

What to optimise

Before we begin, I’d suggest getting initial build time metrics for your project. Here is the Xcode build timing summary we received for the Kickstarter iOS project:

Using build time metrics above we can now find the most time-consuming tasks within the build process and define what can be improved. I’d split the build time optimization techniques in the following way:

  • Build settings optimizations
  • Source code improvements
  • Project enhancements
  • Others

Let’s dive into details!

Build settings optimizations

The first thing we should do is to check that our project is configured optimally for Debug configuration. These settings are pre-filled by default when you create a new project. But it is worth checking ones for an existing project, just in case someone changed them accidentally.

Build Active Architecture Only (ONLY_ACTIVE_ARCH)

If enabled, Xcode creates a binary for the active architecture only. In the development stage, we build the project either on device or simulator (active architecture). The release build should contain all supported architectures because one is shipped via App Store to all variety of users devices. Make sure that Debug is set to Yes and Release is set to No.

Compilation Mode (SWIFT_COMPILATION_MODE)

This setting defines how the Swift files are rebuilt within a module. Set it to Incremental for Debug configuration and only rebuild the Swift source files that are out of date. Use Whole Module for Release to rebuild all Swift source files in the module and apply certain code optimizations.

Optimization Level (SWIFT_OPTIMIZATION_LEVEL)

The Optimization Level setting defines a way we’d like to optimize the build. Code optimizations result in slower build times because of the extra work involved in the optimization process. Debug builds should be configured with No Optimization, since we need a fast compile time. For Release builds it can be set to Optimize for Speed.

Debug Information Format (DEBUG_INFORMATION_FORMAT)

dSYM is a Debug Symbol file, that contains debug information to symbolicate and interpret crash reports. You should always create one for Release builds, but you won’t use it for Debug build most of the times.

Source code improvements

Find code that compiles slowly

As we’ve learned in the previous article, one of the main slowdowns in the compilation process is the evaluation of complex expressions. To help us find areas where the Swift compiler struggled, Xcode can generate a warning for any function or expression that took longer than specified limit to type-check. Here are the results for the Kickstarter project I’ve got with Build Time Analyzer:

Let’s have a look at the biggest offender. The function configureBaseGradientView() was mentioned 20 times in the Xcode build log and each time it took 10067ms to perform type checking.

There is a complex expression with the chain of infix operator |> invocations. No surprises, that it took a while to type check one. As an improvement, we can specify the type explicitly and split it up into two:

Just like that we can fix the rest of the issues and reduce the total build time from 92sec to 57sec.

Reduce the work on rebuild

It is quite important to understand how Swift compiler works and finds the files to recompile. You might probably know that Swift’s dependency model is based around files. If you make a change within a function body, the compiler is smart enough to know that only that file will need to be recompiled. On the other hand, adding or removing a new function or an entity in the file will trigger a re-compilation of all the files that depend on it.

You can do the next to reduce the work that compiler should do on rebuild:

  • define entities in separate files;
  • use correct access modifiers for your classes, structs, enums, extensions, etc.

Remove unused code

As your project evolves there could appear unreachable code that is not used anymore or will never be reached. It has an impact on a project’s build time by slowing it down, thus needs to be found and removed. I’ve described the ways of doing it in one of my previous posts.

Pre-build dependencies

Every project has various dependencies that might slow down the build process. You can increase the build efficiency by using pre-built dynamic frameworks and libraries. Then the framework should be rebuilt if there is a new version available. This approach could be applicable for both internal and external dependencies that are not changed frequently.

Code vs Xibs/Storyboards

You might consider implementing UI from code, instead of using Xib and Storyboard files. In my opinion, both approaches have their pros and cons. Having UI defined in Xib and Storyboard files slows down the clean build and increases the app size. On the other hand, it should be much easier to understand ones rather than complex UI created from code. Speaking about incremental builds, you won’t notice a significant build time difference.

Project enhancements

Improve Run Script phases 

Run script phase is executed whenever you build the project. You can find them on the Build Phases pane:

We have the next run script phases  defined for this target:

  • Swiftlint - runs swiftlint to enforce the coding style and conventions
  • Fabric - runs a script that initializes Crashlytics
  • Carthage - copies frameworks to the application bundle and removes unused architectures

Eventually, it took 6sec to run all of the project’s run script phases. Most probably we shouldn’t run these tasks every time we rebuild the project. Here are the ways we can speed it up:

  • Consider using git commit hooks instead;
  • Skip running a script for Debug configuration or Simulator destination if possible;
  • Declare script’s inputs and outputs or use copy files phase instead.

Adopt Modular Architecture

If you are working on a big project it can be a good idea to split monolithic codebase into modules. Modularizing your code allows the Xcode to compile only the modules you modify and cache those outputs for future builds. Moreover, starting from Xcode 10 we can benefit from building targets in parallel whenever possible. Let’s check the modules and their build times using xcode-build-times-rendering:

As you can see, the project contains 3 framework targets and the application target which is responsible for establishing all dependencies. But we can’t get maximum from the parallelisation because there is a linear dependency between these targets. A better option would be defining independent feature modules:

Modularizing your code this way makes parallel build execution more effective and speeds up the incremental build.

Others

With the above-mentioned approaches, you can speed up your builds time for most of the projects. But these are not all of the techniques you can use. You might consider using the build system like Buck or Bazel and having remote cache for build artifacts.

Conclusion

Long build times slow down your development process and have a direct impact on developers productivity. In this article, we’ve explored how to reduce the work that compiler should do and increase the build efficiency.

How do you keep your projects build time under control? Are you using any of the techniques mentioned in this article? Feel free to reach me out on Twitter and let me know your thoughts or ask questions you might have.

Thanks for reading!