Kmp performance
Skill almasumdev/awesome-kotlin-multiplatform-agent-skills/.github/skills/performance/kmp-performance
Performance tuning for Kotlin Multiplatform — coroutines on iOS, the new memory model, cold-start cost of the shared framework, and avoiding common Kotlin/Native footguns. Use when profiling a KMP app that feels slow on iOS.From its SKILL.md
npx -y skills add almasumdev/awesome-kotlin-multiplatform-agent-skills --skill kmp-performanceAssembled 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.
SKILL.md
4.7 KB, ~1.1k tokens by cl100k_base, as published. Nobody here has run it
KMP Performance
Instructions
Most KMP performance problems surface on iOS because Kotlin/Native compiles AOT to a large framework binary. Fix them in this order: cold start → memory model misuse → dispatcher misuse → allocation hot paths.
1. Cold start
The iOS framework binary is loaded on app launch. Large exported API surfaces and heavy top-level initializers pay at startup.
- Shrink exported symbols. Only export composition roots. Everything else
internal. - Avoid work in top-level
val/objectinitializers. Kotlin/Native runs them eagerly on first access; with a large dependency graph they cascade. Useby lazy(thread-safe by default). - Do not initialize Koin / HTTP / DB at framework load. Wire those on first screen.
- Measure with Instruments → App Launch, and Xcode's
DYLD_PRINT_STATISTICS=1.
2. The new memory model (default since Kotlin 1.9)
The old "freezing" model is gone. Shared mutable state across threads is legal but should still be minimized:
- Prefer
StateFlow/SharedFlowfor cross-thread communication. - Use
kotlinx.atomicfufor atomic counters; it compiles to real atomics on all targets. - Do not add
@SharedImmutable/freeze()— they're deprecated and do nothing useful.
If you see "InvalidMutabilityException" in logs, you are on the legacy MM. Force the new one:
# gradle.properties
kotlin.native.binary.memoryModel=experimental # already default; harmless belt-and-braces
3. Dispatchers on iOS
Dispatchers.Mainmaps to the UIKit main queue — use it only for UI updates.Dispatchers.Main.immediate— dispatches synchronously if already on main. Prefer it for state-flow collectors feeding SwiftUI.Dispatchers.IOexists on Kotlin/Native (coroutines 1.7+) and uses a bounded thread pool. Use for file / socket / blocking work. Do not reach fornewSingleThreadContext { }except for strictly-serial state.Dispatchers.Defaultuses the standard pool for CPU work.
Common mistake: doing all work on Dispatchers.Main because "Swift seems fine" — the main thread serializes with UIKit drawing and causes jank. Always withContext(Dispatchers.IO) { repo.load() }.
4. Hot-path allocations
Kotlin/Native's GC is generational but pauses more than Android ART on large heaps. Reduce allocations in tight loops:
- Prefer
buildList { }over repeated+. - For numeric arrays, use
IntArray/FloatArrayinstead ofList<Int>. - Avoid
String.formaton hot paths — it allocates heavily on iOS. - Cache
Regex,Json, andDateTimeFormatterinstances at top-levelby lazy.
5. Flow performance
flowOn(Dispatchers.Default)upstream expensive maps — otherwise they run on the collector.stateIn(SharingStarted.WhileSubscribed(5_000))deduplicates subscriptions across screens.distinctUntilChanged()on flows whose upstream emits unchanged values.- Do not
.collect { }inside aLaunchedEffect(Unit)that keys onUnitif the flow depends on a changing parameter — you'll leak subscriptions.
6. Serialization & networking
Json { ignoreUnknownKeys = true; explicitNulls = false }at a single top-level instance.- Ktor's
ContentNegotiationreuses theJson— don't re-create per call. - For very large responses, consider
Json.decodeFromStreamon JVM orJson.decodeFromBufferedSourcewithkotlinx-io. Avoid reading the full body intoStringfirst.
7. SQLDelight
- Put queries on
Dispatchers.IO:.asFlow().mapToList(Dispatchers.IO). - Use
transaction { }for multi-write paths — a single transaction is orders of magnitude faster on iOS WAL. - Index columns you
WHEREon; EXPLAIN QUERY PLAN works via the CLI.
8. Profiling
- Android: Android Studio CPU Profiler + Perfetto.
- iOS: Xcode Instruments — Time Profiler for CPU, Allocations for GC churn, Network for Ktor traffic.
- Shared benchmarks:
kotlinx-benchmarkforcommonMainmicrobenchmarks that run on JVM + Native.
Checklist
- Exported API trimmed; top-level initializers are
by lazy. - No frozen/sharedImmutable legacy annotations remain.
- Repository/IO work dispatched on
Dispatchers.IO, neverMain. -
Json/Regex/formatters are cached, not re-instantiated per call. - Flows use
flowOnupstream andstateIn(WhileSubscribed)where appropriate. - SQLDelight queries run on
Dispatchers.IOand multi-write paths usetransaction. - Cold start and a key screen profiled on a real iOS device.
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.