Compose Multiplatform vs Flutter: Rendering, Performance, and Bundle Size

Compose Multiplatform and Flutter can produce similarly smooth mobile interfaces, but they do not reach the screen through the same stack. That difference matters more than the usual “Kotlin versus Dart” comparison.

Compose Multiplatform extends the Compose programming model across platforms. Flutter ships a more self-contained UI stack with its own engine and Dart runtime. Both approaches add machinery around application code, but the cost appears in different places: rendering work, startup, memory, binary size, and platform integration.

One detail is especially easy to get wrong: Skia is not Flutter. Skia is a general-purpose graphics library used by multiple projects. Flutter historically relied heavily on Skia, while current Flutter uses Impeller as its default rendering engine on iOS and Android. Compose Multiplatform uses Compose’s platform-specific implementation on Android and a Skia-based rendering path through Skiko on targets such as iOS and desktop.

The rendering stacks are different

A simplified mobile view looks like this:

Compose Multiplatform
Android:
Compose UI -> Android Compose rendering -> Android graphics stack -> GPU

iOS:
Compose UI -> Skiko / Skia -> Metal-backed graphics path -> GPU

Flutter is more uniform:

Flutter widgets
      |
      v
Flutter framework
      |
      v
Flutter Engine
      |
      v
Impeller
      |
      +---- Metal on iOS
      |
      +---- Vulkan on Android
      |
      v
     GPU

This is one reason “both use Skia” is no longer a useful summary. Flutter’s modern mobile renderer is Impeller. Skia can still appear elsewhere in Flutter’s graphics stack and on supported backends, but it is not the default mobile renderer that defines current Flutter rendering behavior.

Compose Multiplatform takes a less uniform route. On Android, Compose Multiplatform is effectively Jetpack Compose, so it can use the established Android Compose stack rather than shipping the same rendering implementation used on every other target. On iOS, Compose renders its UI through its own canvas-based path backed by Skiko/Skia.

That platform split affects both performance expectations and binary overhead.

Frame time matters more than a headline FPS number

A UI running at 60 Hz has roughly 16.7 ms available per frame. At 120 Hz, the budget drops to about 8.3 ms.

60 Hz  -> 1000 / 60  = 16.67 ms per frame
120 Hz -> 1000 / 120 =  8.33 ms per frame

A framework does not become “faster” simply because a benchmark reports 120 FPS. The useful question is whether composition, layout, drawing, rasterization, and GPU submission consistently stay inside the frame budget under the application’s real workload.

Flutter has an advantage in predictability here: Google controls the framework, engine, and Impeller renderer as one stack. Impeller precompiles a smaller set of shaders during the build process, specifically to reduce runtime shader-compilation stalls that can cause animation jank.

Compose Multiplatform has also moved well beyond its early iOS performance. JetBrains reported with the stable iOS release in Compose Multiplatform 1.8 that startup was comparable with native applications and scrolling was comparable with SwiftUI, including on high-refresh-rate devices. In Compose Multiplatform 1.11, concurrent rendering on iOS became enabled by default, moving rendering work to a dedicated render thread.

Those results make the old assumption that Compose Multiplatform on iOS is inherently slow increasingly difficult to defend. They still do not imply that every CMP application will match every Flutter or native application. Application architecture, recomposition scope, image decoding, list behavior, overdraw, effects, and custom drawing can dominate framework-level differences.

CPU-heavy work is a separate problem

Rendering benchmarks also say little about business logic.

Consider an application that parses a large document, performs encryption, resizes images, or runs a local database query. That work may become the bottleneck before the renderer matters.

frame
 |
 +-- state update
 +-- composition / widget build
 +-- layout
 +-- drawing
 +-- raster / GPU work
 |
 +-- unrelated CPU work --------> can still block useful progress

Compose Multiplatform uses Kotlin and, depending on the target, the relevant Kotlin runtime and compilation model. Flutter release applications compile Dart ahead of time to native machine code on mobile while the Flutter Engine provides the runtime services and graphics integration.

For a real comparison, I would profile the exact workload rather than extrapolate from an empty counter app. A scrolling feed, map-heavy UI, text editor, camera screen, and database client stress completely different parts of the system.

Bundle size exposes the cost of portability

Cross-platform UI code is not free in binary terms.

On Android, Compose Multiplatform has an important structural advantage: its Android target is Jetpack Compose. It does not need to reproduce the exact iOS or desktop rendering stack inside the Android application.

On iOS, Compose Multiplatform needs more of its rendering machinery in the application. JetBrains’ Compose Multiplatform 1.8 benchmark reported roughly 9 MB of additional app size compared with a SwiftUI application containing equivalent UI logic and assets. That figure is useful as a framework benchmark, not as a fixed tax for every application.

Flutter also carries framework overhead. A release application packages the Flutter Engine plus AOT-compiled Dart application code and the assets required by the application. The final download size then depends on architecture slicing, store processing, native plugins, fonts, images, symbols, and other build inputs.

So a table such as “CMP = X MB, Flutter = Y MB” is usually misleading unless both applications were built from the same feature set, for the same target, with the same assets and release settings.

A better model is:

final application size
=
application code
+ framework/runtime code
+ renderer/engine code
+ native dependencies
+ resources and fonts
+ assets
+ architecture/build overhead

The relative contribution of each term changes as the application grows. A 9 MB framework difference is enormous in a tiny utility and much less important in a 200 MB media application.

Desktop changes the calculation again

Desktop packaging makes the contrast more visible.

Compose Multiplatform desktop applications commonly ship JVM/runtime components together with native graphics libraries. Flutter desktop applications ship the Flutter engine and application-specific native artifacts. Neither should be judged from the size of a single executable file because distributable packages include supporting libraries and resources.

The important metric is the artifact users actually download and install:

macOS   -> .app / signed distribution package
Windows -> packaged application / installer
Linux   -> distribution package

For desktop software, runtime availability, packaging format, symbol stripping, bundled fonts, native libraries, and installer compression can move the number enough to invalidate comparisons based on a bare hello-world executable.

Platform integration can outweigh renderer differences

There is another cost that a frame-time chart does not show: crossing into native platform UI.

Both frameworks can embed or interact with native views, but a screen dominated by native maps, camera previews, video, web views, or platform-specific controls no longer measures only the framework’s renderer.

Compose Multiplatform has a particularly interesting trade-off for Kotlin teams. Shared Kotlin code can cover domain logic, networking, persistence, and UI while native APIs remain reachable through Kotlin Multiplatform interop. Android code also stays close to the existing Jetpack Compose ecosystem.

Flutter offers a more uniform UI runtime across platforms. That consistency is valuable when the product wants nearly identical rendering and behavior on Android and iOS, but it also means the Flutter engine is a central part of the application architecture.

Neither model removes platform boundaries. They put those boundaries in different places.

What I would benchmark before choosing

For an actual project, I would build the same representative screen in both frameworks and record:

release artifact size
cold-start time
warm-start time
p50 / p95 / p99 frame time
janky frame count
peak and steady-state memory
CPU during sustained scrolling
battery impact during a realistic session

The test should use release or profile builds on the same physical devices. Debug builds are useful for development, but they are poor evidence for runtime performance comparisons.

I would also test one screen that represents the application’s worst case rather than only its home screen. If the product is a note application, that might be a long document with selection and IME input. For a social client, it might be a media-heavy feed. For a dashboard, it could be several continuously updating charts.

That data is much more useful than deciding from the renderer name alone.

The practical difference

Compose Multiplatform and Flutter are both capable of smooth production UI, but their engineering trade-offs are different.

Compose Multiplatform is especially compelling when Kotlin, Jetpack Compose, and shared application logic are already part of the architecture. Its Android path fits naturally into the Android ecosystem, while iOS and desktop rely more heavily on the multiplatform rendering stack.

Flutter is more vertically integrated. The Flutter framework, engine, Dart runtime, and Impeller give Google tighter control over how a frame moves from widget code to the GPU. That can make rendering behavior more consistent across mobile targets, at the cost of carrying Flutter’s runtime and engine as part of the application.

Bundle size follows the same pattern: neither framework has one meaningful universal number. The useful measurement is the release artifact for the application you are actually building. Renderer architecture explains where some of the overhead comes from; profiling shows whether that overhead matters.