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) │
└──────────────┘ └────────────┘ └────────────┘- 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ể
// ❌ 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
- Run app
- View > Tool Windows > Layout Inspector
- Xem Compose tree và recomposition counts
Profiler
- View > Tool Windows > Profiler
- Chọn CPU
- Record trace
- 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
| 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
- 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