Zygote
UI toolkitsnerd

Compose vs Views, measured

Neither toolkit is unconditionally faster. The honest answer depends on what the screen does, what it costs to build the first frame, and whether you measured or guessed.

11 minAPI 21–37 · 17interactivequizverified 2026-10-27

The question "is Compose faster than Views?" is malformed in the same way as "is a car faster than a truck?". The useful version is: which costs less for this screen, measured on this device, and what does the difference cost you in development time and correctness?

TL;DR#

  • Compose avoids XML inflation and has a flatter runtime tree; it can be faster for dynamic, data-driven screens.1
  • Views can be leaner for simple static screens, and their cost model is easier to predict.
  • Both are bound by the same 16.7 ms frame budget and both render through RenderThread.2
  • The dominant factor is usually not the toolkit but what you do during composition/draw — I/O, allocation, unstable state.
  • Measure with for startup and frame timing, and Baseline Profiles to remove compilation cost from the first run.

Mental model#

Two ways to build a bookshelf.

Kit: pre-cut parts, assembled by position. Fast to assemble for a known shape, awkward when the shape changes.

Workshop: you describe the shelf you want, and a machine cuts and assembles it. Slower to set up for a single shelf, much faster when the shelf changes shape every day.

Where the analogy breaks#

The workshop does not rebuild from scratch each time — it re-cuts only the shelves that changed. Similarly, Compose does not re-inflate the screen; it recomposes the affected scopes. So the analogy's "slower to set up" is only true for the first composition and for large structural changes, which is precisely where benchmarking starts to matter.

The big picture#

Loading diagram…
Text description of this diagram

The shared rendering pipeline beneath both toolkits. A change sets dirty flags, Choreographer schedules work for the next vsync, measure and layout compute sizes and positions, draw records a display list, and sync hands it to RenderThread for GPU execution. Compose adds a composition phase before measure, where the runtime decides which composables to re-execute; the View system adds inflation before the first measure. Both converge on the same measure-layout-draw-sync pipeline and the same 16.7 ms deadline, which is why the comparison is really about what happens before measure, not after it.

The shared bottom half of that diagram is why the differences between toolkits are smaller than advocates on either side claim. Both end up recording display lists and handing them to the same renderer.

Deep dive#

Where the costs actually differ#

CostViewsCompose
First frameXML inflate + reflection + style resolutioncomposition + measure
Structural changeadd/remove views, re-measurerecompose, re-measure
Per-item updateonBindViewHolder, then invalidaterecomposition of the item scope
Layoutmeasure/layout on the view treemeasure/layout on the composition's layout nodes
Drawdisplay list per RenderNodedisplay list per layout node
Runtime treeone object per Viewone group per composable, flatter

The interesting rows are the first two. Inflation is a real cost paid at startup: parsing XML, reflectively constructing views, resolving style attributes. Compose replaces that with function calls — but the first composition still has to allocate and build the slot table.

Startup: the honest comparison#

Cold start has a fixed sequence: process fork, Application.onCreate, activity create, first composition or inflation, first frame. The toolkit affects the middle of that sequence.

A concrete way to measure it:

@get:Rule
val benchmark = MacrobenchmarkRule()
 
@Test
fun coldStartupCompose() = benchmark.measureRepeated(
    packageName = "com.example.notes",
    metrics = listOf(StartupTimingMetric()),
    iterations = 10,
    startupMode = StartupMode.COLD,
) {
    pressHome()
    startActivityAndWait()
}
 
@Test
fun coldStartupViews() = benchmark.measureRepeated(
    packageName = "com.example.notes.views",
    metrics = listOf(StartupTimingMetric()),
    iterations = 10,
    startupMode = StartupMode.COLD,
) {
    pressHome()
    startActivityAndWait()
}

Two builds of the same screen, measured identically. That is the only comparison that means anything, and the result will vary by screen complexity.

Baseline Profiles: the level playing field#

Both toolkits pay a compilation cost on first run. A tells ART which methods to compile ahead of time, so the first launch does not interpret them.3

@OptIn(ExperimentalBaselineProfilesApi::class)
class BaselineProfileGenerator {
    @get:Rule
    val rule = BaselineProfileRule()
 
    @Test
    fun generate() = rule.collect(packageName = "com.example.notes") {
        pressHome()
        startActivityAndWait()
        device.findObject(By.res("notes-list")).fling(Direction.DOWN)
    }
}

Compose benefits substantially from a baseline profile, because composition involves many small generated methods that the JIT would otherwise compile on first use. Without one, a Compose app's first scroll can be noticeably worse than its steady state — a difference that disappears once the profile is in place.

Recomposition cost, concretely#

The expensive thing in Compose is not "recomposition" in the abstract; it is recomposition that should not have happened.

// Every item recomposes when the list identity changes, because List is unstable.
@Composable
fun Feed(posts: List<Post>) {
    LazyColumn {
        items(posts) { post -> PostRow(post) }
    }
}
 
// Stable parameter: PostRow can be skipped when the post is unchanged.
@Composable
fun Feed(posts: ImmutableList<Post>) {
    LazyColumn {
        items(posts, key = { it.id }) { post -> PostRow(post) }
    }
}
 
@Immutable
data class Post(val id: String, val title: String, val body: String)

Three changes — immutable list, stable model, key — and the cost of appending one post drops from "recompose every row" to "compose one row". No View-system equivalent is that sensitive to a type declaration.

Draw cost: where Compose can win#

A deeply nested View hierarchy costs a measure pass per level. Compose's layout nodes are flatter for the same UI because Row/Column/Box do not add platform views.

// A card with a title, subtitle and two actions.
// Views: CardView → ConstraintLayout → 4 children = 6 platform views.
// Compose: Card → Column → Row → Text/Text/Button = layout nodes, no platform views.

Fewer platform objects means less measure work, less memory, and fewer RenderNodes. For lists with complex rows, that difference compounds.

Draw cost: where Views can win#

A screen that is mostly static text can be cheaper in Views if it is simple enough that inflation cost is trivial and the tree is shallow. A TextView has highly optimised text layout; a Compose Text goes through a different stack that is very good but not free.

The honest framing: for a five-view static settings screen, the difference is in the noise. For a scrolling feed with heterogeneous items, Compose's model usually wins because it avoids rebinding an entire view hierarchy per item.

The phase-deferral trick#

This is the biggest performance lever Compose offers and the View system has no clean equivalent:

// Recomposes on every scroll pixel: reads scroll state in composition.
@Composable
fun Header(scrollState: ScrollState) {
    val offset = scrollState.value
    Box(modifier = Modifier.offset(y = (-offset).dp))
}
 
// Reads in layout: no recomposition, only re-layout.
@Composable
fun Header(scrollState: ScrollState) {
    Box(
        modifier = Modifier.offset { IntOffset(0, -scrollState.value) },
    )
}

With the lambda, a fling does layout work per frame instead of composition work. The same trick applies to graphicsLayer { } and drawBehind { }. In the View system, the closest analogue is View.setTranslationY, which also avoids a layout pass — but the compiler cannot tell you when you got it wrong.

Memory and binary size#

ViewsCompose
Runtime objectsone per Viewgroups + layout nodes
Library footprintappcompat, recyclerview, materialcompose runtime, ui, foundation, material3
APK sizegenerally smaller for simple appslarger, though R8 and baseline profiles help
Method countlowerhigher (mitigated by R8)

Compose adds a measurable amount to APK size. On a modern app with Play delivery that is usually acceptable; on a very small utility app it may not be.

What actually makes either slow#

In rough order of impact, from real traces:

  1. I/O on the main thread. Database, disk, network. Toolkit-independent.
  2. Allocation in draw or composition. GC pauses, toolkit-independent.
  3. Unnecessary recomposition or re-layout. Compose-specific, and the most common Compose problem.
  4. Deep measure hierarchies. Views-specific, and the most common View problem.
  5. Missing baseline profile. Affects both, hurts Compose more on first run.
  6. Image decoding on the main thread. Toolkit-independent.

Note that four of the six have nothing to do with the toolkit.

How to run a fair comparison#

If you are going to compare the two toolkits on your own screens, the method matters more than the result:

  1. Same feature, same data, same design. A Compose screen with more images is not a slower toolkit; it is a different screen.
  2. Release build, both. R8, no debug flags, baseline profiles installed for both.
  3. Same device, cold start, ten iterations. Report the median, not the best run.
  4. Measure both startup and scroll. They stress different parts of the stack.
  5. Record the version of everything. A Compose 1.9 comparison is not valid for Compose 1.6.

Without those five, the comparison measures the benchmark, not the toolkit.

Android 17 and memory limits#

Android 17 enforces stricter per-app memory limits and adds generational GC to ART's concurrent collector.4 Both shift the calculus slightly: allocation-heavy UI patterns are more expensive than they were, and a toolkit that allocates less per frame has an advantage. Compose's per-frame allocation is low if written correctly (no lambdas in hot paths, no remember churn), and high if not.

How it evolved#

Interface changes16 releases
APIVersionWhat changed
11.0
  • Pull-down notification shade
  • Home screen with widgets
31.5 Cupcake
  • Home screen widgets and folders
92.3 Gingerbread
  • New UI with dark theme and green accents
113.0 Honeycomb
  • The Holo design language arrives
123.1 Honeycomb
  • Resizable widgets
164.1 Jelly Bean
  • Project Butter: vsync-driven, smooth UI
  • Expandable notifications
204.4W KitKat (Wear)
  • Voice-driven card UI
215.0 Lollipop
  • Material Design
  • Notifications on the lock screen
247.0 Nougat
  • Quick Settings tiles
  • Direct Reply in notifications
268.0 Oreo
  • Notification channels
289 Pie
  • Dark theme groundwork
2910
  • System-wide dark theme
3011
  • Conversation notifications and bubbles
  • Media controls in Quick Settings
3112
  • Material You dynamic colour
3313 Tiramisu
  • Themed app icons
  • Runtime notification permission
3616 Baklava
  • Material 3 Expressive
  • Live Updates notifications

The performance story moved in three stages: Compose 1.0 was slower than Views for many screens; 1.2–1.4 improved the runtime substantially; and the arrival of baseline profiles plus compiler stability diagnostics closed most of the remaining gap. The 2025 Compose-first statement is an ecosystem decision, not a performance claim.

Under the hood#

Curiosity corner#

  • ComposeView is a View. Embedding Compose in a View layout means the composition is measured as a single view; the cost is one extra layer, not one per composable.
  • The first composition is the expensive one. Scrolling and state updates are much cheaper than the initial build, which is why startup benchmarks and scroll benchmarks tell different stories.
  • A baseline profile is worth more than most micro-optimisations. It removes interpretation from the first run of every hot method, including the compiler-generated ones.

Try it#

Frame timing from a release build

adb shell dumpsys gfxinfo com.example.notes framestats

Check whether a baseline profile is installed

adb shell dumpsys package com.example.notes | grep -i profile

Check yourself#

Check yourself

1 / 12

What is the difference between `invalidate()` and `requestLayout()`?

0 answered · 0 correct

Sources#

Sources

Every version number, API level, class name and date on this page is traced to one of these. Pages are re-verified against them; last_verified above says when.

  1. docsCompose performancedeveloper.android.comaccessed 2026-10-27↩
  2. docsRendering | App qualitydeveloper.android.comaccessed 2026-10-27↩
  3. docsBaseline Profiles overviewdeveloper.android.comaccessed 2026-10-27↩
  4. docsBehavior changes: Apps targeting Android 17developer.android.comaccessed 2026-10-27↩
  5. docsCompose phasesdeveloper.android.comaccessed 2026-10-27↩
  6. docsCompose stabilitydeveloper.android.comaccessed 2026-10-27↩
  7. docsMacrobenchmark overviewdeveloper.android.comaccessed 2026-10-27↩
  8. docsการแสดงผลช้า | App qualitydeveloper.android.comaccessed 2026-10-27↩