How did we enhance our app’s compile time?

Romain Brunie
AVIV Product & Tech Blog
5 min readJan 17, 2024

At AVIV, we consistently seek ways to enhance our developers’ experience working on our apps (SeLoger, Immoweb, Immowelt, and Immonet). Compilation time is no exception. Our latest efforts were concentrated on optimizing the compilation time for the white-label features/packages that our apps currently use.

Before attempting to improve compile time, we initiated the process by assessing areas for potential improvement. We employed various methods to accomplish this.

Xcode Build Timeline

We first used Xcode Build Timeline to identify potential bottlenecks that were taking too long to compile. Cleaning our build folder in between builds is crucial so that you can compare them with the performance optimizations you have implemented.

Assets

We identified that on a fresh build, our asset catalog was taking 19 seconds to build and was blocking compilation.

We have a large number of icons imported from Figma, and currently, not all of them are used as not all white-label features have been developed yet.

This issue is still in progress, and we are considering the following alternatives:

  • Should we only import assets that are currently needed?
  • Should we create a catalog per theme so that each module imports only the necessary assets?

It’s worth noting that this issue only affects fresh builds, so is it a genuine concern? Maybe not.

SwiftLint

In our effort to make our build times faster, we discovered that SwiftLint was running twice for every dependency when building our project.

Firstly, we observed its presence in the build phases of our project, and then we identified it as a SwiftLint plugin in the manifest file (Package.swift) for each package:

for target in package.targets {
target.plugins = target.plugins ?? []
target.plugins!.append(.swiftLint)
}

This redundancy meant that SwiftLint was executed once by the target and once more for each dependency, doubling its impact on our build times.

This issue is still under consideration, and we are exploring two alternatives:

  • Should we only run SwiftLint on commit?
  • Should we remove SwiftLint from our dependencies?

Build Timing Summary

We used Build Timing Summary, but we did not identify any areas for improvement here.

Build time compiler flags

We used -Xfrontend -warn-long-function-bodies=200 and -Xfrontend -warn-long-expression-type-checking=200 to gather warnings from the compiler whenever a function or type check takes more than 200 ms.

To set this up, we updated the manifest file (Package.swift) for each package. We enabled our flags in the Swift build settings of each target within a package.

for target in package.targets {
target.swiftSettings = target.swiftSettings ?? []
target.swiftSettings!.append(
.unsafeFlags([
"-Xfrontend", "-warn-long-function-bodies=200",
"-Xfrontend", "-warn-long-expression-type-checking=200"
])
)
}

When we use the -Xfrontend flag with unsafeFlags in a Swift Package Manager configuration, we are providing additional flags that will be passed to the Swift compiler frontend during the build process. This is useful for making certain adjustments or optimizations specific to the compiler's behavior.

Compiling our packages enabled us to pinpoint significant areas for improvement. Our UI white-label features, which are powered by TCA, presented an opportunity to optimize our build time. First, it’s important to note that we follow the performance guidelines outlined by Point-Free.

Type inference with EmptyReducer

Having a view that displays multiple steps could result in the following reducer:

It took 19 seconds for the code to compile from a cleaned build folder. At some point, the build failed because the compiler was unable to type-check this expression in reasonable amount of time.

We updated the code to provide explicit types to the EmptyReducer, which includes some generics:

It took 100ms for the code to compile from a cleaned build folder.

Type inference with XCTestDynamicOverlay unimplemented method

Before TCA 1.5, adding a test value to a dependency could result in the use of the unimplemented method.

static let test: Self = .init(
load: unimplemented(),
loadProjectType: unimplemented(),
store: unimplemented(),
delete: unimplemented(),
userType: unimplemented(),
setNonOwner: unimplemented()
)

With this test value, it took 3 minutes for the code to compile from a cleaned build folder.

We updated the code to provide an explicit type for the test value:

static let test: Self = {
let load: @Sendable () -> OwnerProject? = unimplemented(placeholder: nil)
let loadProjectType: @Sendable () -> OwnerProject.ProjectType? = unimplemented(placeholder: nil)
let store: @Sendable (OwnerProject) -> Void = unimplemented()
let delete: @Sendable () -> Void = unimplemented()
let userType: @Sendable () -> UserType = unimplemented()
let setNonOwner: @Sendable () -> Void = unimplemented()

return .init(
load: load,
loadProjectType: loadProjectType,
store: store,
delete: delete,
userType: userType,
setNonOwner: setNonOwner
)
}()

With this test value, it took less than 10ms for the package to compile from a cleaned build folder.

Since TCA 1.5, the @DependencyClientmacro automatically provides a public initializer for all endpoints along with a default ‘unimplemented’ value for each endpoint.

static let test = OwnerProjectStore()

With this test value, compilation time also reduced to less than 10 ms from a cleaned build folder.

Conclusion

Our exploration revealed several areas where compile times could be improved.

Generics can sometimes contribute to longer compile times, especially when used extensively or in complex ways, as TCA uses them in its stores and reducers.

Updating TCA to the latest version and leveraging its new features improved our build compile time in certain areas without requiring additional code.

As we continue to refine our build process, we remain vigilant in our efforts to find the best approaches for compilation efficiency and deliver a smoother development experience.

We conducted our optimizations with valuable insights gained from following the guidance provided in this informative article.

Kudos to Adrien Simon, Omar Belhaouss, and Daniel Ahlborn, who also contributed to these enhancements.

--

--

Romain Brunie
AVIV Product & Tech Blog

Passionate about Clean Code and Software Craftsmanship @AVIV