Skip to Content

Performance trong Jetpack Compose

Hiểu cách Compose hoạt động giúp bạn viết code hiệu quả và tránh các vấn đề performance.

1. Ba Phases của Compose

Compose render UI qua 3 phases:

┌──────────────┐ ┌────────────┐ ┌────────────┐ │ Composition │ ──▶│ Layout │ ──▶│ Draw │ │ (What) │ │ (Where) │ │ (How) │ └──────────────┘ └────────────┘ └────────────┘
  1. Composition: Chạy composables, xác định what UI cần hiển thị
  2. Layout: Đo lường và định vị, xác định where mỗi element
  3. Draw: Vẽ lên canvas, xác định how render

Skip phases khi có thể

// ❌ Thay đổi trong Composition - chạy cả 3 phases var size by remember { mutableStateOf(100.dp) } Box(modifier = Modifier.size(size)) // ✅ Thay đổi trong Layout only - skip Composition var size by remember { mutableStateOf(100) } Box(modifier = Modifier.layout { measurable, constraints -> val placeable = measurable.measure(constraints) layout(size, size) { placeable.place(0, 0) } }) // ✅ Thay đổi trong Draw only - skip Composition và Layout var alpha by remember { mutableStateOf(1f) } Box(modifier = Modifier.graphicsLayer { this.alpha = alpha })

2. Smart Recomposition

Compose skip unstable composables

// ❌ Recompose mỗi lần parent recompose (unstable) @Composable fun ItemList(items: MutableList<Item>) { // MutableList là unstable // ... } // ✅ Skip được khi items không đổi (stable) @Composable fun ItemList(items: List<Item>) { // List là stable (assuming Item is stable) // ... }

Stable types

// ✅ Automatically stable val primitive: Int val string: String val immutableList: List<String> // ✅ data class với stable fields @Stable // Optional nếu tất cả fields đã stable data class User(val id: Int, val name: String) // ❌ Unstable val mutableList: MutableList<String> class User(var name: String) // Mutable property

@Stable và @Immutable annotations

// @Immutable: Object không bao giờ thay đổi @Immutable data class Product( val id: Int, val name: String, val price: Double ) // @Stable: Object có thể thay đổi nhưng Compose được notify @Stable class CartState { var items by mutableStateOf(listOf<Product>()) }

3. remember đúng cách

Cache expensive calculations

// ❌ Tính mỗi recomposition @Composable fun FilteredList(items: List<Item>, query: String) { val filtered = items.filter { it.name.contains(query) } // Chạy mỗi lần LazyColumn { items(filtered) { ItemRow(it) } } } // ✅ Chỉ tính lại khi inputs thay đổi @Composable fun FilteredList(items: List<Item>, query: String) { val filtered = remember(items, query) { items.filter { it.name.contains(query) } } LazyColumn { items(filtered) { ItemRow(it) } } }

remember object creation

// ❌ Tạo object mới mỗi recomposition @Composable fun MyScreen() { val formatter = SimpleDateFormat("dd/MM/yyyy") // Mới mỗi lần } // ✅ Reuse object @Composable fun MyScreen() { val formatter = remember { SimpleDateFormat("dd/MM/yyyy") } }

4. derivedStateOf cho derived values

// ❌ Trigger recomposition mỗi scroll pixel @Composable fun ScrollFab() { val listState = rememberLazyListState() val showFab = listState.firstVisibleItemIndex > 0 // Tính mỗi scroll if (showFab) { FloatingActionButton(onClick = { }) { } } } // ✅ Chỉ recompose khi điều kiện thay đổi @Composable fun ScrollFab() { val listState = rememberLazyListState() val showFab by remember { derivedStateOf { listState.firstVisibleItemIndex > 0 } } if (showFab) { FloatingActionButton(onClick = { }) { } } }

5. key() cho Lists

Giúp Compose track items

// ❌ Không có key - Compose không biết item nào là item nào LazyColumn { items(users) { user -> UserItem(user) } } // ✅ Có key - Compose track đúng items LazyColumn { items( items = users, key = { it.id } // Unique, stable ID ) { user -> UserItem(user) } }

Tại sao key quan trọng?

  • Khi list thay đổi (thêm, xóa, reorder), Compose biết item nào cần recompose
  • State của mỗi item được giữ đúng
  • Animations hoạt động chính xác

6. Lazy Layout Performance

Avoid heavy work in item composables

// ❌ Heavy work trong item LazyColumn { items(images) { url -> val bitmap = remember { loadBitmap(url) } // Blocking! Image(bitmap) } } // ✅ Load async LazyColumn { items(images) { url -> AsyncImage( model = url, contentDescription = null ) } }

Use contentType

LazyColumn { items( items = feed, key = { it.id }, contentType = { item -> when (item) { is Post -> "post" is Ad -> "ad" is Header -> "header" } } ) { item -> when (item) { is Post -> PostItem(item) is Ad -> AdItem(item) is Header -> HeaderItem(item) } } }

7. Defer Reads - Đọc state càng muộn càng tốt

// ❌ Đọc state sớm - toàn bộ Box recompose @Composable fun AnimatedBox() { var offset by remember { mutableStateOf(0f) } Box( modifier = Modifier .offset(x = offset.dp) // Đọc offset ở Composition ) } // ✅ Đọc state trong Draw phase @Composable fun AnimatedBox() { var offset by remember { mutableStateOf(0f) } Box( modifier = Modifier .offset { IntOffset(offset.roundToInt(), 0) } // Lambda - đọc trong Layout ) } // ✅ Tốt nhất: graphicsLayer (Draw phase only) @Composable fun AnimatedBox() { var offset by remember { mutableStateOf(0f) } Box( modifier = Modifier .graphicsLayer { translationX = offset } // Đọc trong Draw phase ) }

8. Baseline Profiles

Baseline Profiles giúp app khởi động nhanh hơn bằng cách pre-compile các paths quan trọng.

Setup

// build.gradle.kts (app module) plugins { id("androidx.baselineprofile") } dependencies { baselineProfile(project(":baselineprofile")) }

Generate profile

// baselineprofile/src/main/java/BaselineProfileGenerator.kt @RunWith(AndroidJUnit4::class) @LargeTest class StartupBenchmarks { @get:Rule val rule = BaselineProfileRule() @Test fun generateBaselineProfile() = rule.collect( packageName = "com.example.app" ) { startActivityAndWait() // Navigate through critical paths device.findObject(By.text("Products")).click() } }

9. Profiling với Android Studio

Layout Inspector

  1. Run app
  2. View > Tool Windows > Layout Inspector
  3. Xem Compose tree và recomposition counts

Profiler

  1. View > Tool Windows > Profiler
  2. Chọn CPU
  3. Record trace
  4. Analyze composable execution times

Recomposition highlighting

// Enable trong debug builds @Composable fun DebugRecompositions(content: @Composable () -> Unit) { val recomposeCount = remember { mutableStateOf(0) } SideEffect { recomposeCount.value++ } Box( modifier = Modifier.border( width = 1.dp, color = when (recomposeCount.value % 3) { 0 -> Color.Red 1 -> Color.Green else -> Color.Blue } ) ) { content() } }

📝 Tóm tắt

OptimizationCách làm
Skip recompositionDùng stable types, @Stable, @Immutable
Cache calculationsremember(key)
Derived valuesderivedStateOf
List performancekey parameter, contentType
Defer readsLambda modifiers, graphicsLayer
Lazy layoutsAvoid heavy work, use Async loading
Startup timeBaseline Profiles

Performance Checklist

  • Sử dụng stable types (List thay vì MutableList)
  • remember expensive calculations
  • Dùng key cho lazy lists
  • Defer state reads với lambda modifiers
  • Profile với Layout Inspector
  • Generate Baseline Profiles cho release builds
Last updated on