Jetpack Compose Performance Optimization
Jetpack Compose performance is the difference between an app that feels native and one that feels sluggish. While Compose is declarative and developer-friendly, its recomposition model can cause performance issues if you do not understand how it works under the hood.
This guide covers the optimization techniques that matter most in production — from controlling recomposition to tuning lazy layouts and profiling real-device performance. These patterns are based on lessons learned from shipping Compose apps with millions of users.
Understanding Recomposition
Recomposition is Compose’s mechanism for updating the UI when state changes. The runtime re-executes composable functions whose inputs have changed. The key performance insight: unnecessary recompositions are the #1 cause of jank in Compose apps.
// BAD: Entire list recomposes when ANY item changes
@Composable
fun TaskList(tasks: List) {
Column {
tasks.forEach { task ->
// This lambda captures 'tasks', causing recomposition
// of ALL items when the list reference changes
TaskItem(
task = task,
onToggle = { /* ... */ }
)
}
}
}
// GOOD: Use LazyColumn with keys for minimal recomposition
@Composable
fun TaskList(tasks: List) {
LazyColumn {
items(
items = tasks,
key = { it.id } // Stable key prevents unnecessary recomposition
) { task ->
TaskItem(
task = task,
onToggle = { /* ... */ }
)
}
}
}
Stability: The Foundation of Performance
Compose skips recomposition of a composable when all its parameters are stable and unchanged. A type is stable when Compose can compare its instances to detect changes. Consequently, making your data classes stable is the most impactful optimization:
// UNSTABLE: List and other collection types are unstable by default
data class TaskState(
val tasks: List, // List is unstable
val filter: TaskFilter,
val lastUpdated: Date // Date is unstable
)
// STABLE: Use Immutable collections and stable types
@Immutable
data class TaskState(
val tasks: ImmutableList, // kotlinx.collections.immutable
val filter: TaskFilter,
val lastUpdated: Long // Primitive = always stable
)
// Mark classes that won't change after creation
@Stable
class TaskRepository(
private val api: TaskApi,
private val db: TaskDao,
) {
// Compose trusts @Stable — won't trigger recomposition
// when this reference is passed to composables
}
// Add to build.gradle.kts
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.7")
}
// Compose compiler reports — find unstable classes
// build.gradle.kts
composeCompiler {
reportsDestination = layout.buildDirectory.dir("compose_reports")
metricsDestination = layout.buildDirectory.dir("compose_metrics")
}
Lambda and Callback Optimization
Lambdas are a common source of unnecessary recomposition. Every time a composable function executes, new lambda instances are created unless you stabilize them:
// BAD: New lambda created on every recomposition
@Composable
fun ParentScreen(viewModel: TaskViewModel) {
val tasks by viewModel.tasks.collectAsStateWithLifecycle()
TaskList(
tasks = tasks.toImmutableList(),
// This creates a new lambda instance every recomposition
onTaskClick = { task -> viewModel.selectTask(task) },
onDelete = { task -> viewModel.deleteTask(task) }
)
}
// GOOD: Use method references or remember lambdas
@Composable
fun ParentScreen(viewModel: TaskViewModel) {
val tasks by viewModel.tasks.collectAsStateWithLifecycle()
// Method reference — stable, no reallocation
val onTaskClick = remember { { task: Task -> viewModel.selectTask(task) } }
val onDelete = remember { { task: Task -> viewModel.deleteTask(task) } }
TaskList(
tasks = tasks.toImmutableList(),
onTaskClick = onTaskClick,
onDelete = onDelete,
)
}
Lazy Layout Performance
LazyColumn and LazyRow are Compose’s equivalent of RecyclerView. Tuning them correctly is essential for smooth scrolling:
@Composable
fun OptimizedFeed(posts: ImmutableList) {
LazyColumn(
// Pre-compose items ahead of the visible area
beyondBoundsItemCount = 3,
// Content padding instead of Spacer items
contentPadding = PaddingValues(16.dp),
// Item spacing instead of Spacer between items
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
items(
items = posts,
key = { it.id },
// Content type enables better recycling
contentType = { post ->
when {
post.hasImage -> "image_post"
post.isPinned -> "pinned_post"
else -> "text_post"
}
}
) { post ->
// derivedStateOf for scroll-dependent calculations
PostCard(
post = post,
modifier = Modifier.animateItem() // Compose 1.7+
)
}
}
}
// Avoid expensive operations during scroll
@Composable
fun PostCard(post: Post, modifier: Modifier = Modifier) {
// Use SubcomposeLayout sparingly in lists
// Avoid Modifier.graphicsLayer { } with complex calculations
Card(modifier = modifier) {
// Use AsyncImage with placeholder for smooth loading
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(post.imageUrl)
.crossfade(true)
.memoryCachePolicy(CachePolicy.ENABLED)
.build(),
contentDescription = post.title,
modifier = Modifier
.fillMaxWidth()
.aspectRatio(16f / 9f),
contentScale = ContentScale.Crop,
)
Text(post.title, style = MaterialTheme.typography.titleMedium)
}
}
Profiling with Composition Tracing
// Enable composition tracing in debug builds
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) {
// Shows recomposition counts in Layout Inspector
ComposeView.setContent {
// Use Layout Inspector > Recomposition Counts
// to identify hot spots
}
}
}
}
// Custom recomposition counter for specific composables
@Composable
fun RecompositionTracker(name: String) {
if (BuildConfig.DEBUG) {
val count = remember { mutableIntStateOf(0) }
SideEffect { count.intValue++ }
Log.d("Recomposition", "$name: ${count.intValue}")
}
}
Key Takeaways
Optimizing Jetpack Compose performance comes down to three principles: make your data stable (use @Immutable, ImmutableList), minimize recomposition scope (derive state, use keys), and profile on real devices (not emulators). As a result, your Compose apps will render at smooth 60fps even with complex UI hierarchies and large data sets.
Related Reading
- Jetpack Compose Android UI Guide
- Mobile App Performance Optimization
- Kotlin Multiplatform Mobile Guide