Optimizing iOS App Size: Dynamic Frameworks vs. Static Libraries in Modern Swift Projects
As iOS applications grow in scope, modularization becomes essential. Codebases are split into separate modules: networking, design systems, features, and core utilities. But modularization comes with a physical price tag. How you link those modules determines your final binary size and startup latency (time-to-interactive).
In 2026, dynamic linking overhead and bloated app sizes are major hurdles. In this post, we explain how to optimize your build targets by switching between dynamic frameworks and static libraries.
Dynamic vs. Static Linking: A Quick Refresher
- Dynamic Frameworks (
.framework,.xcframework): Linked at runtime. The app binary contains references to the framework, and the dynamic linker (dyld) loads them into memory when the app starts. - Static Libraries (
.a,.frameworkwith static Mach-O type): Linked at compile time. The compiler copies the library’s compiled object code directly into the main app executable.
Dynamic Linking:
[ App Binary ] --- (dyld loads at startup) ---> [ FeatureA.framework ] [ FeatureB.framework ]
Static Linking:
[ App Binary (Includes FeatureA + FeatureB code directly) ]
The Dynamic Linking Penalty: Startup Latency
Every dynamic framework added to your project increases your app’s pre-main execution time. dyld must load the binaries, rebase symbols, bind references, and initialize runtimes. If you have 30+ dynamic modules, your users could experience a 1–2 second launch delay.
Apple recommends keeping the number of dynamic frameworks to a minimum (ideally under 15).
The Static Solution: Binary Size Optimization
Static linking allows the compiler’s linker to perform dead code stripping. If your main app only imports 10% of a static utility library, the other 90% is stripped out during linking. If that library were dynamic, the entire framework would be packaged into the app bundle, bloating the final download size.
Configuring Swift Package Manager (SPM) for Static Linking
By default, SPM package dependencies are compiled dynamically or dynamically wrapping based on system parameters. You can explicitly force library dependencies to compile statically in Package.swift:
// swift-tools-version: 6.0
import PackageDescription
let package = Package(
name: "CoreModule",
platforms: [.iOS(.v17)],
products: [
// Explicitly define static linking
.library(
name: "CoreModule",
type: .static,
targets: ["CoreModule"]
),
],
dependencies: [],
targets: [
.target(
name: "CoreModule",
dependencies: []
),
]
)
The Hybrid Approach: Best of Both Worlds
If you have a complex app with extensions (like widgets, notifications, or watch apps), sharing code gets tricky. If you link a static library to both the main app and a widget extension, that library’s code is duplicated in both binaries, increasing total app store download size.
In this scenario, follow these linking rules:
- Use Static Libraries for internal modules that are only consumed by a single target (e.g., the main app binary).
- Use Dynamic Frameworks only for shared modules consumed by both the main app and your extensions. This ensures the shared code is stored once in the app bundle.
- Turn on Link-Time Optimization (LTO) in Xcode Build Settings. LTO allows the compiler to optimize code across different modules, further reducing binary sizes.