Greetings, traveler!
The second episode of the Swift Academy podcast series on iOS performance is now available.
Together with Walid SASSI and Bogdan Poplauschi, we continued the discussion from the first episode and moved beyond SwiftUI performance into Swift Concurrency, app launch, modularization, and the architectural decisions that influence runtime behavior.
Swift Concurrency does not make an app faster by default
First, we talked about migration to Swift Concurrency.
Replacing callbacks, RxSwift, Combine, or DispatchQueue code with async/await can make code easier to read, but it does not automatically improve performance.
A sequential chain of independent await calls can accidentally serialize work that previously happened in parallel. CPU-heavy transformations can also end up running on the main actor if isolation boundaries are not designed carefully.
We also discussed common escape hatches such as Task, Task.detached, and excessive use of @MainActor.
The compiler accepting the code does not mean the concurrency model is correct. It should still be clear who owns asynchronous work, who can cancel it, and what state that work is allowed to access.
Abstractions have a runtime cost
Another part of the discussion focused on abstractions.
Protocols, type erasure, actors, and architectural boundaries can make a codebase easier to understand and change. That does not make them free.
The cost becomes more relevant when an abstraction is used on a hot path. Paying for an abstraction once during feature setup is very different from paying for it repeatedly while scrolling, rendering views, or processing frequent state updates.
We discussed examples such as heterogeneous feeds, AnyView, AnyPublisher, protocol-based models, actors, and concrete types.
The goal is not to remove abstractions. It is to understand where their cost is paid and whether that cost matters for the application.
What actually happens during app launch
A large part of the episode was dedicated to launch performance.
App launch starts before AppDelegate, SceneDelegate, or the SwiftUI App entry point begins executing application code. The operating system creates the process, dyld prepares binaries and frameworks, runtimes are initialized, and only then does application code start running.
We also discussed the difference between showing the first frame and reaching the first meaningful interaction.
An application can already be visible while the user is still unable to interact with it. From the user’s perspective, launch is not finished yet.
This makes the launch critical path more useful than simply measuring how long didFinishLaunching takes.
Static and dynamic frameworks
We also discussed the impact of static and dynamic linking on the launch process.
With static linking, framework code becomes part of the final executable. Dynamic frameworks remain separate binaries that the dynamic loader has to locate and prepare during launch.
Dynamic frameworks have valid use cases, but they also introduce launch-time work. Choosing between static and dynamic linking therefore involves tradeoffs rather than a universal rule.
We also discussed how dependency managers and large dependency graphs can influence this decision.
Modularization and dependency graphs
Having many modules does not automatically make an application slow. How those modules depend on each other matters much more.
A large set of small, independent modules can be easier to manage than a few modules that eagerly initialize large dependency trees. Third-party SDKs can also introduce transitive dependencies and startup work that are easy to overlook.
Launch performance often exposes architectural problems: unnecessary dependencies, global services, eager initialization, or features that prepare resources long before the user needs them.
Protect the launch path
Launch problems usually accumulate gradually.
One team adds analytics. Another adds feature flags. Someone adds database initialization, remote configuration, or another SDK. Each decision may look reasonable on its own, but together they can turn startup into a long and fragile dependency chain.
We discussed treating launch as something that needs explicit ownership.
A useful rule is simple: work performed during launch should help the user reach the first useful screen or interact with the app sooner. Everything else should be considered for deferred initialization.
This becomes especially relevant with SwiftUI, where startup work can be distributed across the App type, environment dependencies, observable models, view initialization, .task, and onAppear.
Understanding what is behind the abstraction
We finished the episode with a broader discussion about modern iOS development.
SwiftUI, Swift Concurrency, Swift Package Manager, and AI tools let developers work at increasingly high levels of abstraction. That is useful, but it also makes it easier to build software without understanding what happens underneath.
Not every developer needs to know every runtime or linker implementation detail. But understanding the basic execution model behind the tools we use makes performance problems much easier to reason about.
The same principle appeared throughout the episode: use abstractions, but understand what they cost.
