Performance
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
Phần tiêu đề “1. Ba Phases của Compose”Compose render UI qua 3 phases:
┌──────────────┐ ┌────────────┐ ┌────────────┐│ Composition │ ──▶│ Layout │ ──▶│ Draw ││ (What) │ │ (Where) │ │ (How) │└──────────────┘ └────────────┘ └────────────┘- Composition: Chạy composables, xác định what UI cần hiển thị
- Layout: Đo lường và định vị, xác định where mỗi element
- Draw: Vẽ lên canvas, xác định how render
Skip phases khi có thể
Phần tiêu đề “Skip phases khi có thể”// ❌ Thay đổi trong Composition - chạy cả 3 phasesvar size by remember { mutableStateOf(100.dp) }Box(modifier = Modifier.size(size))
// ✅ Thay đổi trong Layout only - skip Compositionvar 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à Layoutvar alpha by remember { mutableStateOf(1f) }Box(modifier = Modifier.graphicsLayer { this.alpha = alpha })2. Smart Recomposition
Phần tiêu đề “2. Smart Recomposition”Compose skip unstable composables
Phần tiêu đề “Compose skip unstable composables”// ❌ Recompose mỗi lần parent recompose (unstable)@Composablefun ItemList(items: MutableList<Item>) { // MutableList là unstable // ...}
// ✅ Skip được khi items không đổi (stable)@Composablefun ItemList(items: List<Item>) { // List là stable (assuming Item is stable) // ...}Stable types
Phần tiêu đề “Stable types”// ✅ Automatically stableval primitive: Intval string: Stringval immutableList: List<String>
// ✅ data class với stable fields@Stable // Optional nếu tất cả fields đã stabledata class User(val id: Int, val name: String)
// ❌ Unstableval mutableList: MutableList<String>class User(var name: String) // Mutable property@Stable và @Immutable annotations
Phần tiêu đề “@Stable và @Immutable annotations”// @Immutable: Object không bao giờ thay đổi@Immutabledata class Product( val id: Int, val name: String, val price: Double)
// @Stable: Object có thể thay đổi nhưng Compose được notify@Stableclass CartState { var items by mutableStateOf(listOf<Product>())}3. remember đúng cách
Phần tiêu đề “3. remember đúng cách”Cache expensive calculations
Phần tiêu đề “Cache expensive calculations”// ❌ Tính mỗi recomposition@Composablefun 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@Composablefun 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
Phần tiêu đề “remember object creation”// ❌ Tạo object mới mỗi recomposition@Composablefun MyScreen() { val formatter = SimpleDateFormat("dd/MM/yyyy") // Mới mỗi lần}
// ✅ Reuse object@Composablefun MyScreen() { val formatter = remember { SimpleDateFormat("dd/MM/yyyy") }}4. derivedStateOf cho derived values
Phần tiêu đề “4. derivedStateOf cho derived values”// ❌ Trigger recomposition mỗi scroll pixel@Composablefun 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@Composablefun ScrollFab() { val listState = rememberLazyListState() val showFab by remember { derivedStateOf { listState.firstVisibleItemIndex > 0 } }
if (showFab) { FloatingActionButton(onClick = { }) { } }}5. key() cho Lists
Phần tiêu đề “5. key() cho Lists”Giúp Compose track items
Phần tiêu đề “Giúp Compose track items”// ❌ Không có key - Compose không biết item nào là item nàoLazyColumn { items(users) { user -> UserItem(user) }}
// ✅ Có key - Compose track đúng itemsLazyColumn { items( items = users, key = { it.id } // Unique, stable ID ) { user -> UserItem(user) }}Tại sao key quan trọng?
Phần tiêu đề “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
Phần tiêu đề “6. Lazy Layout Performance”Avoid heavy work in item composables
Phần tiêu đề “Avoid heavy work in item composables”// ❌ Heavy work trong itemLazyColumn { items(images) { url -> val bitmap = remember { loadBitmap(url) } // Blocking! Image(bitmap) }}
// ✅ Load asyncLazyColumn { items(images) { url -> AsyncImage( model = url, contentDescription = null ) }}Use contentType
Phần tiêu đề “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
Phần tiêu đề “7. Defer Reads - Đọc state càng muộn càng tốt”// ❌ Đọc state sớm - toàn bộ Box recompose@Composablefun AnimatedBox() { var offset by remember { mutableStateOf(0f) }
Box( modifier = Modifier .offset(x = offset.dp) // Đọc offset ở Composition )}
// ✅ Đọc state trong Draw phase@Composablefun 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)@Composablefun AnimatedBox() { var offset by remember { mutableStateOf(0f) }
Box( modifier = Modifier .graphicsLayer { translationX = offset } // Đọc trong Draw phase )}8. Baseline Profiles
Phần tiêu đề “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
Phần tiêu đề “Setup”// build.gradle.kts (app module)plugins { id("androidx.baselineprofile")}
dependencies { baselineProfile(project(":baselineprofile"))}Generate profile
Phần tiêu đề “Generate profile”@RunWith(AndroidJUnit4::class)@LargeTestclass 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
Phần tiêu đề “9. Profiling với Android Studio”Layout Inspector
Phần tiêu đề “Layout Inspector”- Run app
- View > Tool Windows > Layout Inspector
- Xem Compose tree và recomposition counts
Profiler
Phần tiêu đề “Profiler”- View > Tool Windows > Profiler
- Chọn CPU
- Record trace
- Analyze composable execution times
Recomposition highlighting
Phần tiêu đề “Recomposition highlighting”// Enable trong debug builds@Composablefun 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
Phần tiêu đề “📝 Tóm tắt”| Optimization | Cách làm |
|---|---|
| Skip recomposition | Dùng stable types, @Stable, @Immutable |
| Cache calculations | remember(key) { } |
| Derived values | derivedStateOf { } |
| List performance | key parameter, contentType |
| Defer reads | Lambda modifiers, graphicsLayer |
| Lazy layouts | Avoid heavy work, use Async loading |
| Startup time | Baseline Profiles |
Performance Checklist
Phần tiêu đề “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