Kmp binary size
Skill almasumdev/awesome-kotlin-multiplatform-agent-skills/.github/skills/performance/kmp-binary-size
Curated agent skills, conventions, and workflows for building Kotlin Multiplatform (KMP) apps with AI coding agents.
npx -y skills add almasumdev/awesome-kotlin-multiplatform-agent-skills --skill kmp-binary-sizeAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Shrinking the Kotlin/Native iOS framework and Android AAR produced by a KMP shared module — exported API trimming, dead-code elimination, static vs dynamic frameworks, and dependency auditing. Use when the iOS app binary is too big.
SKILL.md
4.7 KB, as published. Nobody here has run it
KMP Binary Size
Instructions
The iOS framework for a non-trivial KMP module typically starts around 8-15 MB and can grow quickly. The main levers are: what you export, how you link, and which dependencies you pull in.
1. Measure first
./gradlew :shared:assembleReleaseFrameworkIosArm64
du -sh shared/build/bin/iosArm64/releaseFramework/Shared.framework/Shared
Break down what's inside:
size -m shared/build/bin/iosArm64/releaseFramework/Shared.framework/Shared | head
bloaty shared/build/bin/iosArm64/releaseFramework/Shared.framework/Shared --demangle=rust
Run ./gradlew :shared:iosArm64MainKlibraries and inspect .klib sizes to see which dependency dominates.
2. Static vs dynamic framework
Static (isStatic = true) is smaller overall when the framework is linked into a single app: the app-side linker strips unreferenced symbols.
iosArm64().binaries.framework { baseName = "Shared"; isStatic = true }
Use dynamic only if multiple app extensions (widget, share sheet) each link the framework — then dynamic avoids duplicating the payload.
3. Trim the exported API
Every public declaration in commonMain ends up in the generated Objective-C header and is retained by the linker.
- Mark helpers
internal. - Put platform-only implementation details in
iosMainasinternal. - For aggregator modules, explicitly
export:
iosArm64().binaries.framework {
baseName = "Shared"
isStatic = true
export(projects.featureAuth) // only these modules' public API reaches Swift
export(projects.featureFeed)
// intermediate modules (core, data) use `implementation(...)` in gradle — not exported
}
Consumers should call into the exported facades; internals are still linked but not visible.
4. Enable release-mode opts
- Kotlin/Native link step runs LLVM passes in release configuration — build with
-Pkotlin.native.cacheKind=nonedisabled (it is enabled by default; don't turn it off for shipping builds). - Ensure
buildTypeisRELEASEfor the Framework you ship:assembleReleaseFramework*.
5. Dependency audit
Common bloat sources:
- Kermit with multiple logger implementations — keep one.
- Ktor logging plugin + full ContentNegotiation serializers — remove in release, or use
ProGuard-style feature flags. - SQLDelight runtime extensions you don't use (coroutines-extensions is fine; paging3 extensions are large).
- kotlinx.datetime's timezone database (
kotlinx-datetime-zoneinfoartifact) — ~200 KB. Include only if you need timezone math beyond the system one.
Prefer implementation(...) over api(...) so consumers don't transitively pull in deps.
6. Android-side considerations
The .aar that androidTarget() produces is usually small, but R8/ProGuard still needs keep rules if you use reflection through kotlinx.serialization:
# consumer-rules.pro
-keepattributes *Annotation*, InnerClasses
-dontnote kotlinx.serialization.AnnotationsKt
-keep,includedescriptorclasses class **$$serializer { *; }
-keepclassmembers class ** { *** Companion; }
-keepclasseswithmembers class ** { kotlinx.serialization.KSerializer serializer(...); }
Ship consumer-rules.pro from :shared so downstream :app doesn't need to duplicate them.
7. Kotlin/Native linking flags
// shared/build.gradle.kts
kotlin {
iosArm64 {
binaries.framework {
isStatic = true
// Strip debug symbols from release framework (dSYM stays separate)
linkerOpts += "-dead_strip"
}
}
}
Combined with static linking, -dead_strip removes sections referenced only by unexported symbols.
8. Swift Package Manager and CocoaPods
When distributing via SPM or CocoaPods, ship the release XCFramework with slices for iosArm64 (device) and iosSimulatorArm64+iosX64 (simulator merged). Don't ship the debug framework — it's 2-3× larger.
Checklist
- Release XCFramework size measured and tracked in CI (fail-on-growth threshold).
-
isStatic = trueunless multiple extensions link the framework. - Only composition/facade modules are
exported; data/core useimplementation. - Unused deps (paging3, zoneinfo, extra logger backends) removed.
-
consumer-rules.proships from:sharedfor Android R8. -
-dead_stripset on the Kotlin/Native binary link. - Shipping framework is release-config, stripped of debug symbols.