Hello, NSLondon.

Sviatoslav
Kliuchev

Lead Software Engineer

JPMorgan Personal Investing

Make your
build time quicker

Why build time matters

Life isn’t endless.

We have plans beyond this build.

CI/CD

Faster feedback on pull requests and releases.

AI loops

AI tests ideas
in a loop

01

Hypothesis

“This should fix the bug”

→
02

Edit

Change the code

→
03

Run

Test the idea

→
04

Feedback

Errors and test results

↶ The next hypothesis depends on the previous result

Loop speed determines how many ideas an agent can test.

Web: more iterations

Edit the code

→

Run it

→

Get feedback

Every step is quick.
Engineers can test more ideas.

iOS: your build is the bottleneck

Edit

→

Compile
+ Link

This is where we wait

→

Run

→

Feedback

10 min / build→≤ 6 iterations / hour

Colleagues with faster builds can test more ideas.

This is an upper bound: editing, running and reviewing results also take time.

How to speed up your builds

01

Type checking

Make the compiler’s job easier

02

Compile cache

Reuse previous compilation results

03

Fewer packages

Merge manifests and keep your modules

04

Tooling

Rethink your build infrastructure

We will measure the first three techniques separately, then combine them.

Type checking:
how long is “reasonable”?

Type inference and overload resolution can make a short expression expensive.

The compiler is unable to type-check this
expression in reasonable time; try breaking up
the expression into distinct sub-expressions

Look familiar?

What changed
in Swift 6.4

The compiler

Rules out unsuitable overloads earlier.

Narrows down possible types and recomputes only affected choices.

SwiftUI in Xcode 27

ContentBuilder reduces the alternatives in builder expressions.

Also works with older deployment targets.

Complex expressions and error diagnostics can still be expensive.

Even so…

Type inference problems
still remain.

Find slow expressions in the build log.

Give intermediate values explicit types.

Split long expressions into smaller pieces.

-Xfrontend -warn-long-expression-type-checking=20-Xfrontend -warn-long-function-bodies=200

Same result. Less guesswork.

Inferred types

func value() -> Double {
    let x = 1 * 2 * 3 * 4
        * 5 * 6 * 7.5
    return x
}

—ms

Explicit types

func value() -> Double {
    let x: Double = 1 * 2 * 3 * 4
        * 5 * 6 * 7.5
    return x
}

—ms

Whole-function type checking · Swift 6.4 · median of — runs · identical output.

Compile cache

Just turn it on.

Two settings in Xcode

In your project’s .xcconfig or Build Settings

COMPILATION_CACHE_ENABLE_CACHING = YES
COMPILATION_CACHE_ENABLE_DIAGNOSTIC_REMARKS = YES

First build

Compiles and stores the result.

Next build

Reuses it when the inputs match.

Check the build log for cache hits. New code, flags or SDKs can cause misses.

Compile cache only:
before and after

— lines of Swift · 101 targets · one Package.swift

Cache OFF

—s

→

Warm cache ON

—s

——actual compiler remarks

Same code. Fresh build / DerivedData and module caches; only CAS retained. — runs, Debug without DWARF.

Cache: paths and teams

Separate branch directories

/work/feature-a → HIT
/work/feature-b → MISS

Our test did not reuse the cache
across different directories.

Xcode 27 supports prefix mapping.
Configure it and verify the hits.

Sharing cache with your team

Enabling the cache
is only the first step.

Remote caching needs a separate
service and integration.

For example, through Tuist.

The path test reflects our configuration without prefix mapping.

Modularization + SPM

A large project. Clean Architecture.

Features

Feature packages

+

Layers

Data · Domain · Presentation

Nice and clean.
But this structure comes at a cost.

How you end up with 100 packages

33features
×
3layers
+
1Shared
=
100packages
Feature01Data    Feature01Domain    Feature01Presentation

The demo also has a root executable package: 101 Package.swift files in total.

Package.swift needs
compiling too

Compile the Swift manifest.

Run it to get the package description.

Resolve dependencies and plan the build.

Minimal manifest on this machine

—s

Compile + link · — runs.
Full SwiftPM CLI: — s.

SwiftPM startup and manifest execution are measured separately. Target source compilation is not included.

This work repeats
for every package

100 × — s≈— s

Estimated time to compile
100 minimal manifests sequentially.

An estimate of total work. Parallelism and shared caches change elapsed build time.

The solution

Fewer packages

One package per feature

Feature01/Package.swift
  target: Data
  target: Domain
  target: Presentation

Or one package for all features

Features/Package.swift
  target: Feature01Data
  target: Feature01Domain
  target: Feature01Presentation
  …

Same results. Different experience.

Keep separate targets, dependencies and the library products you need.

Packages only:
same code, fewer manifests

33 features. 100 library targets + Demo. Compiler cache OFF.

101 manifestsThree packages per feature

—

35 manifestsOne package per feature

—

1 manifestOne internal package

—

—

SwiftPM CLI, macOS, Debug without DWARF. — runs. Fresh build/cache directories; OS file cache retained.

All techniques combined

Same program and output. A synthetic demo project.

Baseline project101 manifests, long expressions, cache OFF

—

Types + packagesOne manifest, short expressions, cache OFF

—

Types + packages + cacheFresh build artifacts, warm CAS

—

—

Synthetic benchmark. Gains in a real project may be smaller.

But what if none of this
gets you the results
you need?

Bazel

What you get

Remote cacheReuse results from colleagues and CI

Remote executionBuild and test on remote workers

Explicit module graphControl targets and dependencies

What it takes

Migration timeRules, toolchains and CI

A steep learning curveInfrastructure and IDE integration

Team retrainingA new workflow to learn and maintain

Build infrastructure needs an owner. Apple actions need a compatible macOS environment.

Low-cost steps
you can take today

01

Enable caching

Rebuild and check the hits.

02

Profile type checking

Find and simplify expensive expressions.

03

Count Package.swift files

Merge the layers of one feature first.

Record the time before each change. Repeat the same scenario afterwards.

Thank you

Demo project

Source code and measurements

LinkedIn

Let’s connect