agentsclimarketplace

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.

Install
npx -y skills add almasumdev/awesome-kotlin-multiplatform-agent-skills --skill kmp-binary-size

Assembled 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 iosMain as internal.
  • 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=none disabled (it is enabled by default; don't turn it off for shipping builds).
  • Ensure buildType is RELEASE for 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-zoneinfo artifact) — ~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 = true unless multiple extensions link the framework.
  • Only composition/facade modules are exported; data/core use implementation.
  • Unused deps (paging3, zoneinfo, extra logger backends) removed.
  • consumer-rules.pro ships from :shared for Android R8.
  • -dead_strip set on the Kotlin/Native binary link.
  • Shipping framework is release-config, stripped of debug symbols.

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.