$npx -y skills add Kotlin/kotlin-agent-skills --skill kotlin-tooling-native-build-performanceDiagnoses and fixes slow Kotlin/Native compilation and linking in Kotlin Multiplatform projects that target iOS. Use when the user reports slow iOS or shared-framework builds, long linkDebug*/linkRelease* or XCFramework tasks, cold CI builds that re-download the Kotlin/Native too
| 1 | # Kotlin/Native Build Performance |
| 2 | |
| 3 | Turn "the iOS build is slow" into a measured diagnosis and a small set of safe |
| 4 | fixes. Two rules apply throughout: |
| 5 | |
| 6 | 1. Never trade away required release behavior. A faster local loop must not |
| 7 | change what CI publishes. |
| 8 | 2. Measure before and after with the same command and the same build state. |
| 9 | An unmeasured fix is a guess. |
| 10 | |
| 11 | ## Step 0: Classify the Slow Scenario |
| 12 | |
| 13 | Establish four facts before editing anything: **where** (local or CI), |
| 14 | **what** (debug feedback loop or release/distribution artifact), **state** |
| 15 | (first build, clean, warm, or no-op), and **phase** (which tasks dominate the |
| 16 | log). Then match the dominant symptom: |
| 17 | |
| 18 | | Symptom in the build log | Likely cause | Read | |
| 19 | |---|---|---| |
| 20 | | `linkRelease*` or `*ReleaseXCFramework` tasks in a local development loop | Building distribution artifacts for development | [artifacts-and-targets](references/artifacts-and-targets.md) | |
| 21 | | Kotlin/Native compiler distribution downloaded on every CI run | `~/.konan` not preserved between runs | [caching-and-gradle](references/caching-and-gradle.md) | |
| 22 | | Long pause before the first task starts | Configuration phase, no configuration cache | [caching-and-gradle](references/caching-and-gradle.md) | |
| 23 | | All iOS targets build when only one simulator is needed | Broad task (`build`, `assemble`, `assemble*XCFramework`) or unused targets | [artifacts-and-targets](references/artifacts-and-targets.md) | |
| 24 | | `ksp*` tasks ahead of `compileKotlinIos*` | Generated-code work on the native path | [exports-and-generated-code](references/exports-and-generated-code.md) | |
| 25 | | Small source edit recompiles and relinks everything | Compiler caches disabled, or missing incrementality | [caching-and-gradle](references/caching-and-gradle.md), [experimental](references/experimental.md) | |
| 26 | | Machine overloaded while several `link*` tasks run at once | Parallel native linking | [caching-and-gradle](references/caching-and-gradle.md), worker-limit caveat | |
| 27 | |
| 28 | ## Step 1: Audit and Measure |
| 29 | |
| 30 | 1. Run the static audit from the project root: |
| 31 | |
| 32 | ```bash |
| 33 | scripts/audit-native-build.sh /path/to/project |
| 34 | ``` |
| 35 | |
| 36 | It is read-only and prints `file:line` findings (disabled caches, broad |
| 37 | local tasks, `transitiveExport`, broad KSP configuration, missing CI |
| 38 | `.konan` cache), each pointing at the reference file with the fix. |
| 39 | Findings are leads, not verdicts — confirm each against project policy. |
| 40 | 2. Find the command the user actually waits for: a script, a CI step, or the |
| 41 | Gradle invocation inside an Xcode build phase. Optimize that command, not |
| 42 | a task you picked yourself. |
| 43 | 3. Run it twice when practical. The first build downloads Kotlin/Native |
| 44 | components and fills caches; only the second and later runs are |
| 45 | representative. Attribute time per task before blaming the compiler: |
| 46 | |
| 47 | ```properties |
| 48 | kotlin.build.report.output=file # writes build/reports/kotlin-build/ |
| 49 | ``` |
| 50 | |
| 51 | Gradle's `--scan` or `--profile` work too. |
| 52 | 4. If you cannot run the build (no macOS host, no Xcode), analyze logs, build |
| 53 | scans, or checked-in metrics instead — and state explicitly that the |
| 54 | conclusion is static. |
| 55 | |
| 56 | ## Step 2: Fix in Safe Order |
| 57 | |
| 58 | Apply fixes one at a time, re-measuring as you go: |
| 59 | |
| 60 | 1. **Restore healthy defaults** — remove cache/daemon workarounds, enable |
| 61 | Gradle build and configuration caches, keep `~/.konan` warm in CI, update |
| 62 | Kotlin: [references/caching-and-gradle.md](references/caching-and-gradle.md) |
| 63 | 2. **Build only what the feedback loop needs** — one specific task per loop, |
| 64 | correct integration method, justified target matrix: |
| 65 | [references/artifacts-and-targets.md](references/artifacts-and-targets.md) |
| 66 | 3. **Cut export and generated-code cost** — drop `transitiveExport`, narrow |
| 67 | `export(...)`, scope KSP work to the native compilations that need it: |
| 68 | [references/exports-and-generated-code.md](references/exports-and-generated-code.md) |
| 69 | 4. **Experimental switches last, with the user's agreement**: |
| 70 | [references/experimental.md](references/experimental.md) |
| 71 | |
| 72 | ## Worked Example |
| 73 | |
| 74 | A developer on an Apple Silicon Mac complains that "every shared-module |
| 75 | change costs 12 minutes". Their loop runs `./gradlew :shared:assembleXCFramework`. |
| 76 | A build scan of the second (warm) run shows: |
| 77 | |
| 78 | ``` |
| 79 | :shared:linkReleaseFrameworkIosArm64 348s |
| 80 | :shared:linkReleaseFrameworkIosX64 |