Why a one-line change rebuilds so much of a big app

Where the time goes when you change one line in a large app and hit build, and what you can do about each part today.

You fix a typo in a label, press Run, and wait. On a small app the wait is a few seconds and nobody thinks about it. On a large app the same one-line change can take minutes, dozens of times a day. The change was tiny. Why isn’t the build?

Because “rebuild what changed” is harder than it sounds, and the compiler is only part of the work. Here is where the time goes, roughly in the order a build spends it.

1. Recompiling more files than you touched

The compiler keeps a record of which declarations each file defines and which ones it uses. When you change a file, it uses those records to decide which other files need to compile again.

  • A change inside a function body usually recompiles just that file. Nothing else could have seen the change.
  • A change to what a file declares (a new property, a changed method signature, a new enum case) recompiles that file and every file that uses what changed. If the type is a model, a view model or a helper used across the app, that can be a large part of the app.
  • A change to a module’s public interface reaches across modules: the modules that import it have to be checked again, and their files that use the changed declarations recompile.

So the same “one line” can mean one file or hundreds, depending on where the line is. Developers learn this by feel (“don’t touch that shared file before lunch”), but the build log rarely explains it.

2. Type checking that is slow on its own

Some code is simply slow to compile, every time its file is recompiled. The usual culprits are expressions where the compiler has to try many type combinations: long chains of operators mixing literals and optionals, large collection literals without a declared type, and very large view bodies built from many nested closures.

None of this is a bug in your code, but in a file you edit all day, a slow expression costs you on every build.

3. Linking the whole app again

After any file recompiles, the app’s binary has to be linked again, and the linker works on the whole app, not on the file that changed. The bigger the app, and the more modules and libraries it links in, the longer that step.

4. Everything after the compiler

Then come the steps that run after linking, some of them on every build:

  • Run-script phases. A script phase that doesn’t declare its input and output files runs on every build, and the build log warns about it. Linters, code generators and version-stamping scripts are common examples.
  • Resources. Asset catalogs, storyboards and other resources are usually skipped when unchanged, but a script that touches them, or a generated file that changes on every build, forces them to run again.
  • Signing and embedding. The app and its embedded frameworks are signed again whenever the binary changes.
  • Install and launch. The built app is copied into the Simulator and started.

Each of these is small on its own. Together, on a large app, they add up to a wait that has little to do with the size of your change.

What you can do today

Most of this can be measured and some of it can be fixed, with tools you already have.

Measure first. Build with a timing summary (Product › Perform Action › Build With Timing Summary, or xcodebuild -showBuildTimingSummary) and open the build timeline in the report navigator. It shows which tasks ran and how long each took. Our guide to the build timeline walks through it. To see the total time of every build in the activity bar, run:

defaults write com.apple.dt.Xcode ShowBuildOperationDuration -bool YES

Find slow type checking. Add these to Other Swift Flags in your Debug configuration, and the compiler will warn about any function or expression that takes longer than the limit (in milliseconds) to type-check:

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

Adding an explicit type, or splitting a long expression or view body into smaller pieces, usually fixes the worst offenders.

Give every script phase its inputs and outputs. Then the build system can skip the script when nothing it reads has changed. If a script really must run every time, make it fast, or move it to a configuration you don’t build all day.

Keep shared declarations stable. Types that half the app uses are the most expensive ones to change. Splitting them into a separate module with a small public interface limits how far a change can reach. This is the idea behind modularizing a large app: it pays off when the interfaces are small and stable, and it adds overhead when they aren’t.

Don’t clean unless you have to. A clean build throws away everything the build system knows about your project, and the next build starts from zero.

What’s left

Do all of this and a large app still has a floor: the everyday rebuild where the change is small and the build still isn’t. That’s the part we’re working on with Quickfire, which makes everyday builds of large apps faster on the project you already have. Early access opens in early 2027.