Dalam artikel ini kita akan membahas optimasi Jetpack Compose, khususnya bahwa recomposition, state management, dan lazy layout memengaruhi performa UI.
Dengan panduan terstruktur ini kita akan memahami mekanisme compose, metrik yang harus diukur, teknik mengurangi recomposition, optimasi LazyColumn/Row, alat profiling, contoh kasus, dan checklist penerapan.
Memahami Recomposition dan Model State pada Jetpack Compose
Composable adalah fungsi deklaratif yang menggambarkan UI dan bereaksi terhadap perubahan state. Jetpack Compose menggunakan snapshot system untuk melacak pembacaan state sehingga recomposition terjadi ketika snapshot yang diamati berubah dan hanya composable yang membaca snapshot itu yang dijadwalkan ulang.
💻 Mulai Belajar Pemrograman
Belajar pemrograman di Dicoding Academy dan mulai perjalanan Anda sebagai developer profesional.
Daftar SekarangRecomposition tidak selalu berarti redraw penuh; ia bisa berhenti pada subtree terbatas jika dependensi tercatat dengan tepat.
Alur umumnya: nilai state berubah → sistem membuat snapshot baru → Compose mendeteksi observer yang terpengaruh → scheduler men-trigger recomposition pada composable terkait. Karena itu, penting memisahkan state scope agar perubahan lokal tidak memaksa recomposition global.
Jenis state meliputi mutableStateOf dan tipe pembungkus State, serta interop dengan Flow atau LiveData menggunakan collectAsState atau observeAsState. Pilih model yang sesuai untuk lifetime dan thread-safety.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
/* contoh: recomposition luas vs terbatas */ @Composable fun ScreenBad() { var counter by remember { mutableStateOf(0) } Column { repeat(100) { ListItem(counter) } // semua item membaca counter => semua recomposed Button(onClick = { counter++ }) { Text("Inc") } } } @Composable fun ScreenGood() { var counter by remember { mutableStateOf(0) } Header(counter) // hanya Header recomposed LazyColumn { items(items) { item -> ListItem(item) } } } |
Biaya recomposition meliputi penggunaan CPU, tekanan pada GC akibat alokasi ulang, serta kenaikan layout dan measure passes jika struktur berubah. Indikator masalah adalah frame drops, UI yang terasa belum mulus, dan spike CPU pada thread UI. Ini adalah sinyal bahwa perlu profiling dan pengurangan scope recomposition.
Mengukur Performa dan Menemukan Bottleneck Compose
Sebelum optimasi, tentukan metrik tujuan, seperti jumlah jank, distribusi frame time, dan penggunaan CPU atau baterai untuk fitur yang sedang diuji; metrik jelas mempersempit hipotesis saat melacak bottleneck. Pilih target ambang agar tidak semua fluktuasi kecil menjadi tugas optimasi.
Gunakan Layout Inspector untuk melihat hitungan recomposition per composable, buka Android Profiler untuk timeline CPU dan thread, lalu ambil System Trace atau ekspor ke Perfetto untuk analisis detail. Catat skenario pengguna yang mereplikasi masalah sebelum merekam trace agar hasil relevan.
Rekam trace dengan menekan rekam pada Android Profiler pada aktivitas bermasalah, jalankan interaksi, lalu hentikan. Dalam Perfetto, cari spike panjang (frame berat) atau pola berulang banyak puncak kecil (banyak recomposition).
Bedakan frekuensi recomposition (banyak event cepat) dengan pekerjaan composable yang mahal (satu event panjang). Jika spike muncul bersamaan dengan render/layout/measure, fokus ke pengurangan kerja dalam fase tersebut; jika banyak recomposition, fokus ke model state dan pemisahan state.
Contoh cepat: trace menunjukkan paket spike 200ms pada layout measure; langkah pertama: profil composable besar, pindahkan format/parsings ke background atau remember hasilnya. Rekomendasi awal: hoist state, batasi scope recomposition, dan gunakan LazyColumn dengan item yang terpisah.
1 2 3 4 5 6 7 8 |
import android.os.Trace @Composable fun ItemWithTelemetry(id: String, content: String) { Trace.beginSection("ItemCompose:$id") // rendering ringan — beratnya dipindahkan ke ViewModel/worker Text(text = content) Trace.endSection() } |
Lakukan hal-hal berikut.
- Cek metrik target di awal dan catat baseline.
- Rekam trace sesuai skenario nyata.
- Identifikasi spike: panjang vs. frekuensi.
- Periksa Layout Inspector untuk recomposition hotspot.
- Pastikan kerja berat keluar dari composition.
- Tambahkan Telemetry custom jika metrik fitur diperlukan.
Prinsip Dasar Mengurangi Recomposition dan Overdraw
Fokus pertama: jaga state tetap lokal kecuali benar-benar perlu diangkat. Pola state hoisting dan single source of truth memastikan satu sumber data—misalnya ViewModel—mengontrol state, sementara composable hanya menerima nilai dan callback. Gunakan model data immutable agar perubahan jelas dan mudah dilacak.
Praktik sehari-hari: pakai remember untuk cache objek berbiaya, derivedStateOf untuk menghitung turunan state tanpa memicu recomposition berlebih, dan pecah UI menjadi small composable supaya recomposition terlokalisasi. Minimalkan lambda captures agar perubahan yang tidak relevan tidak menyebabkan recomposition pada tree besar.
Overdraw terjadi saat piksel digambar berkali-kali; identifikasi dengan Layout Inspector atau Profile GPU Rendering. Kurangi dengan menghapus background redundan, gabungkan layer, dan gunakan warna latar pada container bukan tiap child. Pindahkan kerja berat, seperti parsing atau perhitungan ke ViewModel atau Coroutines (Dispatchers.IO).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// BEFORE: recomposition dan work di composable @Composable fun BigScreen(items: List) { val expensive = items.map { parse(it.raw) } // heavy work LazyColumn { items(items) { it -> ItemRow(it) } } } // AFTER: state di- hoist, cache, key untuk list @Composable fun BigScreen(viewModel: BigViewModel) { val items by viewModel.items.collectAsState() val parsed by remember(items) { derivedStateOf { items.map { parse(it.raw) } } } LazyColumn { items(items, key = { it.id }) { item -> ItemRow(item) } } } |
Strategi Optimasi Jetpack Compose untuk Lazy Layout
LazyColumn dan LazyRow hanya me-render item yang terlihat, beda dengan Column atau Row yang menggambar semua anak; ini mengurangi overdraw dan memori untuk list panjang. Untuk stabilitas, selalu sediakan keys pada items agar perubahan dataset tidak memicu recomposition penuh. Jika ada beberapa tipe tampilan, beri contentType supaya sistem bisa melakukan recycling lebih efisien.
Hindari menggunakan modifier berat pada tiap item—misalnya animasi dalam graphicsLayer atau drop shadow yang dibangun per item; lebih baik pre-render atau gunakan composable sederhana. Untuk prefetching dan windowing, atur initialPrefetchItemCount bila tersedia, dan desain indexing pada lapisan paging agar permintaan data tidak burst sekaligus.
Penanganan gambar: pakai placeholders, batasi ukuran eksplisit, dan gunakan pola lazy image loading libraries yang membatalkan loading saat item keluar layar. Sticky headers dan animations memperkaya UX, tetapi matikan bila memperburuk frame time pada ribuan item.
1 2 3 4 5 6 7 8 |
val state = rememberLazyListState() LazyColumn(state = state) { items(itemsList, key = { it.id }, contentType = { it.type }) { item -> Box(modifier = Modifier.height(72.dp)) { // tampilan item ringan } } } |
- Checklist: tambahkan keys, set contentType, kurangi Modifier berat.
- Uji menggunakan Android Studio profiler dan metrik frame untuk ribuan item.
- Jika masih lambat, optimalkan data layer sebelum mengubah UI lebih dalam.
Teknik Lanjutan Menggunakan remember derivedStateOf Dan snapshotFlow
remember menyimpan nilai komputasi dalam lifecycle composable, sedangkan derivedStateOf membungkus perhitungan turunan agar hanya menghasilkan invalidation ketika inputnya berubah. Gunakan derivedStateOf saat perhitungan turunan relatif mahal, tetapi jarang berubah; pilih snapshotFlow ketika kamu perlu menjembatani state Compose ke coroutine atau Flow tanpa memicu recomposition berlebih.
Praktik cepat: bungkus perhitungan turunan dengan derivedStateOf dan scope dengan remember plus key; pakai snapshotFlow dalam LaunchedEffect untuk side-effect asinkron. Contoh sebelum/sesudah sebagai berikut.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// sebelum @Composable fun ListScreen(items: List, query: String) { val filtered = items.filter { it.matches(query) } // recomposed tiap kali LazyColumn { /* ... */ } } // sesudah @Composable fun ListScreen(items: List, query: String) { val filteredState = remember(query, items) { derivedStateOf { items.filter { it.matches(query) } } } val filtered by filteredState LazyColumn { /* ... */ } } |
Trade-off: derivedStateOf mengurangi CPU, tapi bisa menambah memori jika menyimpan salinan besar; snapshotFlow memperkenalkan kompleksitas race jika coroutine tidak dikelola dengan baik, jadi selalu gunakan pembatalan otomatis, seperti collectLatest.
Untuk debugging, aktifkan recomposition counts dalam Android Studio atau log akses derived state; jika perhitungan masih tereksekusi terlalu sering, periksa key pada remember dan pastikan tidak ada referensi objek baru tiap frame.
Rekomendasi: selalu scope derivedStateOf dengan remember + key, buat perhitungan murni (no side-effects), dan gunakan snapshotFlow hanya untuk bridging ke coroutine.
Profiling Alat Debug dan Checklist Deploy untuk Performa Compose
Android Profiler, Layout Inspector (tab Compose), Perfetto/trace, dan Macrobenchmark adalah kumpulan alat inti untuk menemukan sumber latensi pada UI dan penggunaan CPU. Mereka saling melengkapi: profiler untuk metrik real-time, inspector untuk memahami recomposition dan tree, serta trace untuk analisis frame-level dan GPU.
- Reproduksi bug dalam perangkat debug agar jejak perilaku konsisten sebelum merekam.
- Rekam trace fokus pada frame timing, hitungan recomposition, serta durasi layout dan measure.
- Lakukan binary search pada perubahan kode untuk mengisolasi regresi performa secara cepat.
- Hapus operasi berat dari composable; pindahkan ke ViewModel atau worker background.
- Pastikan list, seperti LazyColumn punya keys dan contentType bila diperlukan.
- Tambahkan monitoring untuk frame drops dan runtime error UI sebelum rilis.
- Benchmark fitur utama pada perangkat kelas bawah untuk memastikan kelayakan.
Jika regresi muncul setelah rilis, rencana mitigasi cepat: revert atau feature flag, kumpulkan trace dari pengguna, lalu lakukan patch berdasarkan hasil binary search. Untuk alur tim, tekankan code review khusus performa dan jadwalkan sesi pair profiling sebelum merge fitur besar; ini seperti inspeksi mekanik sebelum balapan—investasi kecil yang mencegah kerusakan besar.
1 2 3 4 5 6 7 8 9 10 11 12 |
class ScrollBenchmark { @get:Rule val benchmarkRule = MacrobenchmarkRule() @Test fun scrollList() = benchmarkRule.measureRepeated( packageName = "com.example.app", metrics = listOf(FrameTimingMetric()), iterations = 5 ) { startActivityAndWait() performScroll() } } |
Penutup
Artikel ini menjanjikan peta jalan praktis untuk menurunkan latensi UI dan penggunaan CPU pada aplikasi Compose. Dengan menerapkan pola state yang tepat, membatasi recomposition, dan mengoptimalkan Lazy Layouts serta rutin mem-profiling, tim bisa mencapai UI lebih responsif dan hemat sumber daya. Gunakan checklist dan contoh pada setiap bagian sebagai panduan implementasi.