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.
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#
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#
| Cost | Views | Compose |
|---|---|---|
| First frame | XML inflate + reflection + style resolution | composition + measure |
| Structural change | add/remove views, re-measure | recompose, re-measure |
| Per-item update | onBindViewHolder, then invalidate | recomposition of the item scope |
| Layout | measure/layout on the view tree | measure/layout on the composition's layout nodes |
| Draw | display list per RenderNode | display list per layout node |
| Runtime tree | one object per View | one 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#
| Views | Compose | |
|---|---|---|
| Runtime objects | one per View | groups + layout nodes |
| Library footprint | appcompat, recyclerview, material | compose runtime, ui, foundation, material3 |
| APK size | generally smaller for simple apps | larger, though R8 and baseline profiles help |
| Method count | lower | higher (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:
- I/O on the main thread. Database, disk, network. Toolkit-independent.
- Allocation in draw or composition. GC pauses, toolkit-independent.
- Unnecessary recomposition or re-layout. Compose-specific, and the most common Compose problem.
- Deep measure hierarchies. Views-specific, and the most common View problem.
- Missing baseline profile. Affects both, hurts Compose more on first run.
- 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:
- Same feature, same data, same design. A Compose screen with more images is not a slower toolkit; it is a different screen.
- Release build, both. R8, no debug flags, baseline profiles installed for both.
- Same device, cold start, ten iterations. Report the median, not the best run.
- Measure both startup and scroll. They stress different parts of the stack.
- 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#
| API | Version | What changed |
|---|---|---|
| 1 | 1.0 |
|
| 3 | 1.5 Cupcake |
|
| 9 | 2.3 Gingerbread |
|
| 11 | 3.0 Honeycomb |
|
| 12 | 3.1 Honeycomb |
|
| 16 | 4.1 Jelly Bean |
|
| 20 | 4.4W KitKat (Wear) |
|
| 21 | 5.0 Lollipop |
|
| 24 | 7.0 Nougat |
|
| 26 | 8.0 Oreo |
|
| 28 | 9 Pie |
|
| 29 | 10 |
|
| 30 | 11 |
|
| 31 | 12 |
|
| 33 | 13 Tiramisu |
|
| 36 | 16 Baklava |
|
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#
- Choreographer.javathe frame clock both toolkits schedule against
- ViewRootImpl.javathe traversal and sync that both toolkits end up in
- the shared renderer: RenderNode, display lists, RenderThread
- View.javathe measure/layout/draw contract Compose interoperates with
Curiosity corner#
ComposeViewis 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#
adb shell dumpsys gfxinfo com.example.notes framestats
adb shell dumpsys package com.example.notes | grep -i profile
Check yourself#
Check yourself
What is the difference between `invalidate()` and `requestLayout()`?
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.
- docsCompose performancedeveloper.android.comaccessed 2026-10-27↩
- docsRendering | App qualitydeveloper.android.comaccessed 2026-10-27↩
- docsBaseline Profiles overviewdeveloper.android.comaccessed 2026-10-27↩
- docsBehavior changes: Apps targeting Android 17developer.android.comaccessed 2026-10-27↩
- docsCompose phasesdeveloper.android.comaccessed 2026-10-27↩
- docsCompose stabilitydeveloper.android.comaccessed 2026-10-27↩
- docsMacrobenchmark overviewdeveloper.android.comaccessed 2026-10-27↩
- docsการแสดงผลช้า | App qualitydeveloper.android.comaccessed 2026-10-27↩