Skip to Content
Course content

236: Build Time Optimization Techniques

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've been working on this project for a few weeks now, and I'm starting to notice something annoying. Every time I change a single string in a view, I'm waiting nearly 20 seconds for the build to finish. For a project of this size, that's a disaster. It breaks my flow. So, I'm going to set aside some time today to figure out where the bottleneck is and fix it.

The "Wait, why is this line slow?" Moment

First, I want to see if I can isolate a specific piece of code that's taxing the compiler. I suspect it's my NetworkConfiguration object. I've written this heavily nested dictionary that handles API endpoints, headers, and timeout settings all in one go. It looks like this:

let config: [String: Any] = [
    "api": [
        "version": "v1",
        "endpoints": [
            "user": ["path": "/user", "method": "GET", "timeout": 30],
            "auth": ["path": "/auth", "method": "POST", "timeout": 60],
            "upload": ["path": "/upload", "method": "POST", "timeout": 300]
        ],
        "headers": ["Content-Type": "application/json", "Accept": "application/json"]
    ],
    "retryPolicy": ["maxAttempts": 3, "delay": 2.0]
]

It looks clean to me, but the Swift compiler has to work incredibly hard to infer the types for every single nested element in that [String: Any] mess. I'm going to try a quick experiment. I'll replace this loosely typed dictionary with a set of nested structs. If the build time for this file drops, I know the type checker was struggling.

I rewrite it into a Configuration struct with nested types. I hit Cmd+B. The build is noticeably snappier. Why? Because I've moved the burden from the type inference engine (which has to guess and check) to the type checker (which just verifies). Explicit types are almost always faster to compile than complex inferred ones.

The "Everything" File Problem

Next, I'm looking at my project navigator. I noticed I have a file called AppUtils.swift that's nearly 2,000 lines long. It contains extensions for String, Date, UIView, and URLSession. I figured keeping them together was "organized," but I suspect it's doing the opposite for the compiler.

Whenever I change one tiny helper method in a String extension, the compiler has to re-evaluate the entire AppUtils.swift file. Since so many other files import these utilities, it can trigger a cascade of recompilations.

I'm going to split this. I'll create String+Utils.swift, Date+Utils.swift, and so on. Now, when I tweak a date formatter, only the date utility file is dirty. I'm observing that my incremental build times—the builds that happen after I've already launched the app—have dropped from 12 seconds to about 3 seconds. It's a huge win for a bit of tedious file moving.

Hunting the Silent Killers

I still feel like there's some bloat. I want to know exactly which functions are taking the longest to compile. Swift has some hidden flags that can tell us this, though they aren't exposed in the standard Xcode UI. I'm going to dive into the Build Settings and add a specific flag to Other Swift Flags:

-Xfrontend -warn-long-function-bodies=100

This tells the compiler: "If any function takes longer than 100ms to type-check, give me a warning." I trigger a build, and immediately, a warning pops up on a complex closure in my data mapping layer. The line is a massive chain of .map and .flatMap calls. The compiler is basically looping through a million permutations to figure out the return type of that chain.

The fix here is simple: I break the chain. Instead of one giant expression, I'll use intermediate variables and explicitly declare their types. It's slightly more verbose, but the compiler no longer has to "solve" the equation; I've given it the answers.

The Dependency Tax

Finally, I'm looking at my dependencies. I'm using a few large libraries, and I've noticed that every time I clean the build folder, it takes forever to get back to a runnable state. I realize I'm importing entire modules when I only need one or two classes.

While Swift doesn't have "selective imports" in the same way some languages do, I can optimize how my modules are structured. I've started moving my core logic into a separate local Swift Package. By isolating the stable business logic into its own module, Xcode can cache that module as a binary. Now, when I'm iterating on the UI, the core logic doesn't even need to be looked at by the compiler. It's already "baked."




📋 Practical Task

Exercise: Optimizing the "Heavy" Analytics Engine

You have been handed a legacy AnalyticsManager.swift file that is causing significant build delays. Your goal is to apply the optimization techniques discussed in this lesson to bring the build time down.

The Scenario:

  • The Type Inference Trap: There is a let eventDefaults: [String: Any] dictionary with 4 levels of nesting. Task: Refactor this into a nested struct hierarchy to eliminate the Any type and help the type checker.
  • The God File: The AnalyticsManager.swift file also contains 500 lines of String and Int extensions used for formatting analytics keys. Task: Move these extensions into separate files (e.g., String+Analytics.swift) to improve incremental build performance.
  • The Complex Chain: There is a function that processes a raw JSON array using a single, massive chain of .compactMap { ... }.filter { ... }.sorted { ... }.map { ... }. Task: Break this chain into three distinct steps with explicit type annotations for the intermediate arrays.

Success Criteria: Your refactored code must compile without warnings, remove all instances of [String: Any] in the configuration, and separate the utility extensions from the main manager class.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.