Compose Multiplatform's Skia Rendering on iOS: Profiling Metal Shader Compilation Stalls, Texture Atlas Thrashing, and the Platform-Specific Patterns That Actually Hit 120fps on ProMotion
TL;DR
Compose Multiplatform on iOS renders through Skiko (Skia for Kotlin), bypassing UIKit’s layout engine entirely and drawing straight to Metal. You get a unified rendering model, but you also inherit iOS-specific problems: first-frame Metal shader compilation stalls, texture atlas thrashing under memory pressure, and missed ProMotion deadlines from naive composition. I’ll walk through the profiler-driven workflow to diagnose each of these, and explain where dropping to native UIKitView is the right call instead of fighting the renderer.
How Skiko actually renders on iOS
Most developers treat Compose Multiplatform as a black box. If you want to hit 120fps, you can’t afford to.
On Android, Compose draws through the platform’s hardware-accelerated Canvas, backed by Skia internally within the OS. On iOS, Compose Multiplatform bundles its own Skia instance via Skiko, which targets Metal directly. There’s no UIKit layout pass. No Core Animation implicit transactions. Your @Composable tree produces Skia draw commands that compile to Metal shader programs and render into a CAMetalLayer.
Why does this matter? iOS has no equivalent of Android’s RenderThread pre-compiling shaders in the background. Every novel shader variant hits the Metal compiler on first use, on the main thread.
Metal shader compilation: the first-frame problem
The single most common source of Compose Multiplatform jank on iOS is shader compilation stalls. 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 shader variant.
Diagnosing with Instruments
Open Xcode Instruments and attach the GPU and Metal System Trace templates. You’re looking for:
MTLCreatePipelineStatecalls exceeding 2ms in the Metal System Trace- Frame gaps in the GPU track where command buffer submission is delayed
- Main thread hangs in the Time Profiler correlating with Skia’s
GrMtlPipelineStateBuilder
The fix: shader warmup
Force Skia to compile its most common shader variants during your splash screen or loading phase. 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.
Texture atlas thrashing
Skia manages a texture atlas for glyph caching and small image regions. On Android with larger GPU memory budgets, this rarely causes issues. On iOS, particularly older devices, memory pressure triggers atlas eviction and reupload cycles that show up as periodic frame drops.
| 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 |
When you see periodic stutter every few seconds, check Metal Resource Allocations in Instruments. If GPU memory allocation/deallocation is spiking, your atlas is thrashing. Reduce the number of distinct font sizes and image scales in hot paths. Prefer integer-scaled images that map cleanly to atlas regions.
Hitting 120fps on ProMotion
ProMotion displays dynamically adjust between 1-120Hz. At 120fps, your frame budget is 8.3ms, roughly half of 60fps.
Minimize recomposition scope. On Android, a dropped frame at 60fps is noticeable but survivable. At 120fps, two dropped frames are visible as a hitch. Use derivedStateOf, stable keys, and @Immutable annotations aggressively to prevent unnecessary recomposition from propagating into draw calls.
Watch for software rendering fallbacks. Certain Skia operations on iOS, particularly complex path clipping combined with RenderEffect blur, can fall back to CPU rasterization. Profile with Metal System Trace to confirm GPU-side rendering for your critical animation paths.
Pre-allocate draw resources. Create Paint and Path objects outside of draw lambdas. Object allocation during the draw phase adds GC pressure that compounds at higher frame rates.
When to drop to native UIKit views
What most teams get wrong: they treat UIKitView interop as a failure state. It isn’t. It’s an architectural tool.
Use native UIKitView for:
- Maps, because MapKit is GPU-optimized in ways you cannot replicate through Skia
- Video playback, since
AVPlayerLayerworks with the OS compositor directly - Web content, where
WKWebViewworks best as a native embed - Text input, because iOS keyboard interop through native
UITextFieldavoids a category of IME edge cases
In my experience building production systems, teams that try to force complex platform-specific components through the Skia renderer spend weeks debugging edge cases that disappear with a single UIKitView wrapper. I’ve watched it happen more than once, and it’s always the same story: someone eventually writes the native wrapper anyway, just later and more frustrated.
What to do with all this
Warm Metal shaders at launch. Render an offscreen canvas exercising your common draw operations during the splash screen to front-load PSO compilation and eliminate first-interaction hitches.
Profile with Metal System Trace, not just frame counters. Frame time averages hide shader compilation spikes and atlas thrashing. Use Xcode Instruments to identify per-frame GPU stalls before you start optimizing Compose code. I’ve seen teams spend days rearranging recomposition boundaries when the real problem was a single unwarmed shader variant.
Use UIKitView for platform-optimized components without guilt. Maps, video, web content, and text input are better served by native views. Fight the renderer only where cross-platform consistency delivers measurable product value, not because it feels like the “right” approach.