DEV Community

SoftwareDevs mvpfactory.io
SoftwareDevs mvpfactory.io

Posted on • Originally published at mvpfactory.io

Compose Multiplatform's Skia Rendering on iOS

---
title: "Compose Multiplatform iOS: Fixing Skia Rendering for 120fps"
published: true
description: "Profile and fix Compose Multiplatform frame drops on iOS. Metal shader warmup, texture atlas tuning, and when to drop to native UIKit views."
tags: kotlin, ios, mobile, performance
canonical_url: https://blog.mvpfactory.co/compose-multiplatform-ios-fixing-skia-rendering-for-120fps
---

## What You Will Learn

By the end of this walkthrough, you will know how to diagnose and fix the three most common sources of frame drops in Compose Multiplatform on iOS: Metal shader compilation stalls, texture atlas thrashing, and missed ProMotion deadlines. You will also know exactly when dropping to native `UIKitView` is the right architectural call.

## Prerequisites

- A working Compose Multiplatform project targeting iOS
- Xcode with Instruments installed
- A physical iOS device with ProMotion (iPhone 13 Pro or later)
- Familiarity with Kotlin and basic Compose concepts

## Step 1: Understand How Skiko Renders on iOS

Here is the gotcha that will save you hours: Compose Multiplatform on iOS is **not** using UIKit's layout engine. It bundles its own Skia instance via Skiko, targeting Metal directly. Your `@Composable` tree produces Skia draw commands that compile to Metal shader programs and render into a `CAMetalLayer`. No UIKit layout pass. No Core Animation implicit transactions.

On Android, the OS handles Skia internally and pre-compiles shaders on a background `RenderThread`. On iOS, every novel shader variant hits the Metal compiler **on the main thread**.

| Factor | Android (Pixel 7+) | iOS (iPhone 13+) |
|---|---|---|
| Skia backing | OS-integrated Skia | Bundled Skiko/Skia |
| Shader compilation | Background `RenderThread` | Main thread via Metal |
| Texture atlas memory | Managed by OS compositor | App-level Metal allocations |
| 120fps target | 8.3ms frame budget | 8.3ms frame budget (ProMotion) |
| GPU profiling | Android GPU Inspector | Xcode Metal System Trace |

## Step 2: Diagnose Shader Compilation Stalls

The single most common source of jank is shader compilation. When Skia encounters a new draw configuration — a novel blend mode, clip shape, or gradient type — Metal must compile a pipeline state object (PSO). That can block for several milliseconds per variant.

Open Xcode Instruments and attach the **GPU** and **Metal System Trace** templates. Look for:

1. `MTLCreatePipelineState` calls exceeding **2ms** in the Metal System Trace
2. Frame gaps in the GPU track where command buffer submission is delayed
3. Main thread hangs in the Time Profiler correlating with Skia's `GrMtlPipelineStateBuilder`

The docs do not mention this, but frame time averages will hide these spikes completely. You need per-frame GPU stall data.

## Step 3: Warm Shaders at Launch

Here is the minimal setup to get this working. Force Skia to compile its most common shader variants during your splash screen. Render an offscreen canvas that exercises your typical draw operations: rounded rectangles, blurred shadows, gradient fills, text with different styles. This front-loads PSO compilation before users see interactive content.

## Step 4: Fix Texture Atlas Thrashing

If you see periodic stutter every few seconds, check **Metal Resource Allocations** in Instruments. Spiking GPU memory allocation/deallocation means your atlas is thrashing — Skia's glyph cache and small image regions are being evicted and reuploaded.

The fix: reduce distinct font sizes and image scales in hot paths. Prefer integer-scaled images that map cleanly to atlas regions.

## Step 5: Hit 120fps on ProMotion

At 120fps your frame budget is **8.3ms** — roughly half of 60fps. Two dropped frames are visible as a hitch. Let me show you a pattern I use in every project:

- Use `derivedStateOf`, stable keys, and `@Immutable` annotations aggressively to minimize recomposition scope
- Watch for software rendering fallbacks — complex path clipping combined with `RenderEffect` blur can fall back to CPU rasterization
- Pre-allocate `Paint` and `Path` objects outside of draw lambdas to reduce GC pressure at higher frame rates

Profile with Metal System Trace to confirm GPU-side rendering for your critical animation paths.

## Step 6: Know When to Use Native UIKitView

This is where most teams go wrong: they treat `UIKitView` interop as a failure. It is an architectural tool. Use native `UIKitView` for:

- **Maps** — MapKit is GPU-optimized in ways you cannot replicate through Skia
- **Video playback**`AVPlayerLayer` works with the OS compositor directly
- **Web content**`WKWebView` works best as a native embed
- **Text input** — native `UITextField` avoids a category of IME edge cases

## Gotchas

- **Do not profile with simulators.** Metal behavior on simulators does not match real devices. Always use a physical ProMotion device.
- **Frame time averages lie.** A smooth 119fps average can hide a 40ms shader compilation spike that users absolutely feel. Use per-frame traces.
- **Atlas thrashing looks like a recomposition bug.** Teams spend days rearranging recomposition boundaries when the real problem is a single unwarmed shader variant or too many font sizes.
- **Fighting the renderer for platform-specific components wastes weeks.** Someone eventually writes the native `UIKitView` wrapper anyway — just later and more frustrated.

## Wrapping Up

Warm your Metal shaders at launch, profile with Metal System Trace instead of frame counters, and use `UIKitView` for platform-optimized components without guilt. Fight the renderer only where cross-platform consistency delivers measurable product value.
Enter fullscreen mode Exit fullscreen mode

Top comments (0)