Profiling #
Di dalam rekayasa kinerja perangkat lunak, ada aturan emas pertama yang wajib kita taati: jangan pernah melakukan optimasi tanpa data hasil profiling. Mengubah baris kode, merestrukturisasi widget tree, atau merancang ulang alur data hanya berdasarkan “asumsi” bahwa bagian tersebut lambat adalah tindakan spekulatif yang membuang-buang waktu pengembangan. Sering kali, apa yang kita duga sebagai penyebab aplikasi melambat ternyata berjalan sangat cepat, sementara bottleneck yang sesungguhnya tersembunyi rapat di area yang sama sekali tidak kita curigai.
Profiling adalah aktivitas merekam, menganalisis, dan membedah konsumsi sumber daya komputasi aplikasi kita secara real-time saat dijalankan. Flutter menyediakan paket peralatan analisis kinerja kelas dunia yang sangat matang bernama Flutter DevTools. Melalui DevTools, kita dapat mengintip durasi rendering setiap frame piksel, memantau penggunaan CPU per fungsi, melacak siklus memori RAM, hingga mengidentifikasi kebocoran memori. Di dalam artikel ini, kita akan mempelajari taktik profiling secara sistematis menggunakan Flutter DevTools guna menemukan akar masalah jank (antarmuka yang tersendat) pada aplikasi kita.
Profile Mode: Prasyarat Utama Profiling #
Sebelum kita membuka DevTools dan mulai merekam metrik, kita harus memastikan bahwa aplikasi kita dijalankan pada mode kompilasi yang tepat. Flutter mendukung tiga mode utama: Debug, Profile, dan Release.
Ada aturan mutlak yang tidak boleh dilanggar: jangan pernah melakukan profiling kinerja pada Debug Mode. Pada debug mode, Flutter menyertakan banyak overhead untuk mendukung fitur Hot Reload, pemeriksaan asersi (assert statements), dan kode diagnostik runtime Dart VM. Hasil perekaman pada debug mode akan menunjukkan performa yang sangat lambat dan tidak mencerminkan apa yang dialami oleh pengguna asli aplikasi kita di produksi.
Untuk mendapatkan data yang akurat, kita harus menjalankan aplikasi pada Profile Mode. Profile mode mengompilasi kode Dart kita menjadi kode mesin native yang dioptimalkan penuh (setara dengan Release Mode), namun tetap mempertahankan saluran diagnostik (tracing ports) agar dapat terhubung dengan Flutter DevTools.
Gunakan perintah terminal berikut untuk menjalankan aplikasi pada profile mode:
# 1. Jalankan aplikasi di perangkat fisik pada Profile Mode
flutter run --profile
# 2. Build biner paket uji berkinerja tinggi untuk didistribusikan ke perangkat tes
flutter build apk --profile # Untuk Android
flutter build ios --profile # Untuk iOS
[!IMPORTANT] Wajib Menggunakan Perangkat Fisik Asli: Saat melakukan profiling, hindari penggunaan emulator Android atau simulator iOS komputer kita. Emulator memetakan instruksi CPU perangkat mobile ke CPU arsitektur komputer (x86/ARM host) yang biasanya memiliki daya komputasi jauh lebih tinggi daripada chip seluler asli. Selain itu, emulator tidak menggunakan pipa rendering GPU asli seluler secara akurat. Selalu hubungkan perangkat fisik asli (disarankan menggunakan perangkat dengan spesifikasi rendah atau low-end device) untuk mendapatkan gambaran performa terburuk yang sesungguhnya.
Performance View: Membedah Frame Chart #
Ketika pengguna mengeluhkan bahwa “layar terasa patah-patah saat digulir (scrolling jank)”, alat pertama yang harus kita buka di Flutter DevTools adalah tab Performance. Halaman ini menampilkan visualisasi grafik batang (bar chart) yang disebut Frame Chart.
Frame Chart menampilkan sejarah rendering setiap frame piksel yang digambar oleh Flutter. Sumbu vertikal (Y) mewakili durasi waktu yang dibutuhkan untuk menggambar frame tersebut (dalam milidetik / ms), sedangkan sumbu horizontal (X) mewakili urutan frame dari waktu ke waktu.
Memahami Batas Waktu Frame (Frame Budget) #
Agar mata manusia menangkap gerakan animasi yang mulus (smooth), aplikasi harus menggambar frame baru pada kecepatan minimal 60 frame per detik (FPS). Kita dapat menghitung batas waktu pengerjaan (budget) untuk setiap frame:
- Kecepatan 60 FPS: Setiap frame harus selesai digambar dalam waktu maksimal 16.6 milidetik ($1.000\text{ ms} / 60\text{ frame}$).
- Kecepatan 90 FPS: Setiap frame harus selesai dalam waktu maksimal 11.1 milidetik.
- Kecepatan 120 FPS (Perangkat modern / High Refresh Rate): Setiap frame harus selesai dalam waktu maksimal 8.3 milidetik.
Di dalam Frame Chart DevTools, kita akan melihat grafik batang dengan tiga indikasi warna:
- Batang Hijau: Frame berhasil digambar di bawah batas waktu budget (kurang dari 16.6 ms untuk layar 60 Hz). Pengguna melihat animasi berjalan mulus.
- Batang Biru: Frame melebihi 16.6 ms namun masih berada di bawah 33 ms. Animasi sedikit terhambat namun tidak terlalu mengganggu.
- Batang Merah (Jank): Frame membutuhkan waktu pengerjaan di atas batas waktu kritis. Ketika ini terjadi, frame terlewat (dropped frame), dan pengguna akan merasakan layar tersendat (jank).
UI Thread vs Raster Thread #
Setiap batang frame di DevTools terbagi menjadi dua bagian pengerjaan thread utama:
- UI Thread (Dart VM): Bagian ini bertanggung jawab menjalankan kode Dart kita. Skenario di dalamnya meliputi pemrosesan event, kalkulasi state, pembangunan widget tree (build), penentuan ukuran tata letak (layout), dan perekaman instruksi menggambar (paint).
- Raster Thread (GPU Engine): Bagian ini bertanggung jawab mengambil instruksi menggambar dari UI thread, mengubahnya menjadi perintah native mesin grafis (Impeller/Skia), dan mengirimkannya ke kartu grafis (GPU) fisik perangkat untuk digambar ke layar.
Arsitektur Alur Pembagian Kerja Frame Budget #
Untuk memvisualisasikan bagaimana UI thread dan Raster thread berkolaborasi membagi batas waktu 16.6ms demi menghasilkan satu frame piksel yang stabil, perhatikan diagram alur di bawah ini:
graph TD
Start["Pemicu Frame Baru (Vsync Signal)"] --> UI["UI Thread (Dart Code)"]
UI -->|1. Inisiasi Build & Layout| Build["Build & Layout (Widget Tree)"]
Build -->|2. Rekam Perintah Menggambar| Paint["Paint (RenderObjects)"]
Paint -->|Kirim Lay-out Layer| Raster["Raster Thread (GPU Engine / Impeller)"]
Raster -->|3. Konversi ke Instruksi GPU| GPU["GPU Driver Rendering"]
GPU -->|4. Tampilkan Gambar di Layar| Screen["Layar Perangkat (Display)"]
subgraph Budget ["Batas Waktu Frame (60 FPS = 16ms)"]
UI
Raster
endJika salah satu dari kedua thread di atas melebihi porsinya hingga total waktu melampaui batas 16.6ms, batang frame akan berubah merah di DevTools. Memeriksa thread mana yang memakan waktu terlama adalah langkah diagnostik awal yang sangat penting:
- Jika bagian UI thread yang panjang: Masalah utama ada di dalam kode Dart kita (misal: rebuild berlebihan atau komputasi matematika rumit di thread utama).
- Jika bagian Raster thread yang panjang: Masalah ada di tingkat GPU (misal: memuat gambar dengan resolusi terlalu besar, atau menggunakan efek grafis berat seperti Opacity/Blur yang membebani GPU).
CPU Profiler: Menganalisis Flame Chart #
Ketika kita menemukan frame berwarna merah di Frame Chart akibat UI thread yang terlalu lambat, kita dapat mengklik batang merah tersebut dan membuka tab CPU Profiler untuk melihat Flame Chart.
Flame Chart adalah visualisasi hierarki panggilan fungsi (call stack) dari waktu ke waktu. Sumbu horizontal (X) menunjukkan durasi waktu pengerjaan, sedangkan sumbu vertikal (Y) menunjukkan kedalaman tumpukan pemanggilan fungsi. Fungsi yang berada di posisi paling atas memanggil fungsi yang ada di bawahnya (Caller $\rightarrow$ Callee).
Cara Membaca Total Time vs Self Time #
Saat menganalisis Flame Chart, kita akan disajikan tabel data yang berisi metrik penting berikut:
- Total Time (Waktu Total): Durasi total yang dihabiskan untuk mengeksekusi suatu fungsi beserta dengan seluruh anak fungsi (callee) yang dipanggil di dalamnya.
- Self Time (Waktu Mandiri): Durasi murni yang dihabiskan untuk mengeksekusi kode di dalam fungsi itu sendiri, tanpa menghitung waktu pengerjaan anak fungsi di dalamnya.
[!TIP] Mencari Bottleneck melalui Self Time: Sebuah fungsi yang memiliki
Total Timebesar belum tentu merupakan penyebab lambat. Bisa jadi ia memanggil fungsi lain yang lambat. Namun, jika sebuah fungsi memilikiSelf Timeyang tinggi, fungsi tersebut adalah akar masalah (bottleneck) yang sesungguhnya. Fungsi tersebut menghabiskan banyak siklus CPU di baris kodenya sendiri. Fokuskan optimasi kita pada fungsi-fungsi dengan nilaiSelf Timetertinggi.
Menambahkan Marker Kustom ke Timeline #
Terkadang, sangat sulit menemukan fungsi buatan kita sendiri di antara ribuan fungsi internal framework Flutter di dalam Flame Chart. Untuk mempermudah pencarian, kita dapat menandai fungsi kita secara manual menggunakan pustaka dart:developer agar memunculkan label khusus di DevTools:
// lib/features/products/data/repositories/product_repository_impl.dart
import 'dart:developer' as developer;
import '../../domain/entities/product.dart';
class ProductRepositoryImpl {
Future<List<Product>> fetchHeavyProducts() async {
// 1. Mulai marker sinkronisasi timeline dengan label kustom
developer.Timeline.startSync('ProductRepository:ParsingHeavyJson');
try {
final String jsonRaw = await _loadJsonFromAssets();
// Operasi yang dicurigai berat
final List<Product> list = _parseBigJson(jsonRaw);
return list;
} finally {
// 2. Wajib akhiri marker di dalam blok finally agar marker ditutup meskipun terjadi error
developer.Timeline.finishSync();
}
}
List<Product> _parseBigJson(String json) {
// Logika parsing...
return [];
}
Future<String> _loadJsonFromAssets() async => '[]';
}
Ketika kita menjalankan profiling, label 'ProductRepository:ParsingHeavyJson' akan muncul sebagai baris blok berwarna di bagian atas timeline DevTools, sehingga kita dapat langsung mengkliknya untuk mengukur durasi pengerjaan fungsi tersebut secara presisi.
Contoh Visualisasi Call Stack Flame Chart #
Berikut adalah gambaran bagaimana Flame Chart menyusun tumpukan panggilan fungsi dari atas ke bawah. Blok yang memiliki bentang horizontal terlebar mewakili operasi yang memakan waktu paling lama:
graph TD
Build["Fungsi build() <br/> Total: 50ms, Self: 5ms"] --> Layout["Fungsi layout() <br/> Total: 10ms, Self: 10ms"]
Build --> BuildList["Fungsi _buildList() <br/> Total: 35ms, Self: 15ms"]
BuildList --> ListTile["Fungsi ListTile() <br/> Total: 20ms, Self: 20ms"]Dengan mengamati struktur pohon di atas, kita dapat menyimpulkan bahwa ListTile adalah area bottleneck murni karena memiliki nilai Self Time yang setara dengan Total Time-nya (20ms), sedangkan fungsi build() di atasnya hanya lambat karena harus menunggu selesainya proses rendering di tingkat bawah.
Memory Profiler: Melacak Kebocoran Memori (Memory Leak) #
Masalah performa tidak hanya berupa tampilan animasi yang patah-patah (jank). Masalah lain yang jauh lebih berbahaya adalah penumpukan memori RAM yang terus meningkat seiring waktu penggunaan aplikasi, yang pada akhirnya akan memicu penghentian paksa aplikasi oleh sistem operasi karena kehabisan memori (OOM - Out of Memory Crash).
Kebocoran memori (memory leak) terjadi ketika objek yang sudah tidak lagi digunakan oleh aplikasi (misalnya halaman Screen yang sudah ditutup) tidak dapat dibersihkan oleh mesin Garbage Collector (GC) Dart karena masih ada referensi aktif dari luar yang menunjuk ke objek tersebut.
Alur Deteksi Leak dengan Heap Snapshot Diff #
Untuk melacak objek mana yang bocor di Flutter DevTools, kita menggunakan fitur Heap Snapshot pada tab Memory. Taktik pelacakannya adalah sebagai berikut:
- Snapshot 1 (Base): Buka aplikasi, masuk ke halaman beranda, lalu klik tombol Take Snapshot di DevTools untuk merekam seluruh objek aktif saat ini di memori.
- Memicu Aksi: Masuk ke halaman yang dicurigai bocor (misalnya Halaman Detail Produk), lakukan interaksi di sana, lalu kembali (pop) ke halaman beranda.
- Garbage Collection (GC): Klik ikon tempat sampah (Trigger GC) di DevTools sebanyak 2-3 kali untuk memaksa Dart membersihkan semua objek yang tidak terpakai dari memori RAM.
- Snapshot 2 (Target): Klik tombol Take Snapshot kembali untuk merekam kondisi memori saat ini.
- Diffing: Pilih tab Diff di DevTools, lalu bandingkan Snapshot 2 dengan Snapshot 1.
Perhatikan daftar kelas yang memiliki nilai Delta positif (jumlah instansi objek bertambah). Jika setelah kita keluar dari Halaman Detail Produk, delta untuk kelas ProductDetailScreen atau RenderObject terkait bernilai positif (+1 atau lebih), berarti halaman tersebut mengalami kebocoran memori.
Contoh Memory Leak Klasik & Cara Memperbaikinya #
Kebocoran memori paling sering disebabkan oleh kelalaian pengembang dalam membatalkan pendaftaran aliran data (Stream Subscription) atau penghenti animasi (Animation Controller) saat widget dihancurkan (dispose).
Berikut adalah contoh kode yang mengalami kebocoran memori karena listener tidak dibersihkan:
// lib/features/dashboard/presentation/screens/dashboard_screen.dart
import 'dart:async';
import 'package:flutter/material.dart';
class DashboardScreen extends StatefulWidget {
final Stream<int> notificationStream;
const DashboardScreen({super.key, required this.notificationStream});
@override
State<DashboardScreen> createState() => _DashboardScreenState();
}
class _DashboardScreenState extends State<DashboardScreen> {
int _unreadNotificationsCount = 0;
StreamSubscription<int>? _subscription;
@override
void initState() {
super.initState();
// Mendengarkan aliran data
_subscription = widget.notificationStream.listen((count) {
setState(() {
_unreadNotificationsCount = count;
});
});
}
// ANTI-PATTERN: Lupa mendefinisikan metode dispose() untuk membatalkan subscription.
// Aliran data stream akan terus menunjuk ke callback widget ini di memori RAM,
// mencegah _DashboardScreenState dibersihkan oleh Garbage Collector meskipun screen ditutup.
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(child: Text('Notifikasi: $_unreadNotificationsCount')),
);
}
}
Berikut adalah perbaikan kode yang benar dengan membatalkan pendaftaran di dalam blok dispose():
// Perbaikan kode di dalam kelas _DashboardScreenState
@override
void dispose() {
// 1. Batalkan pendaftaran stream subscription secara aman
_subscription?.cancel();
// 2. Selalu panggil super.dispose() di akhir pembersihan
super.dispose();
}
Setiap kali kita membuat controller (seperti TextEditingController, ScrollController, AnimationController) atau melakukan listening pada stream/ChangeNotifier di dalam StatefulWidget, kita wajib menyertakan logika pembersihannya pada metode dispose().
Widget Inspector: Menganalisis Rebuild Komponen #
Ketika masalah jank berada pada UI thread dan kita mencurigai bahwa penyebabnya adalah proses pembangunan ulang widget (widget rebuild) yang terjadi terlalu sering pada area yang tidak perlu, kita dapat menggunakan Flutter Widget Inspector.
Di dalam tab Flutter Inspector, kita dapat mengaktifkan beberapa fitur visualisasi bantuan:
- Track Widget Builds (Performance Tab): Saat diaktifkan, DevTools akan menghitung secara real-time berapa kali setiap widget di aplikasi kita memanggil fungsi
build(). Jika sebuah widget statis (seperti ikon atau teks) terhitung memanggil build ratusan kali saat kita menggulir layar, itu adalah indikasi kesalahan manajemen state. - Highlight Repaints: Fitur ini akan memunculkan garis batas berkedip dengan warna acak di sekeliling widget yang sedang digambar ulang (repainted) di layar fisik perangkat. Jika saat kita mengetuk tombol kecil, seluruh area layar ikut berkedip warna-warni, berarti area gambar kita terlalu luas dan tidak terisolasi dengan baik.
- Show Guidelines: Memunculkan garis pembatas tata letak (layout boundaries) di layar perangkat untuk mempermudah kita menganalisis apakah ada widget yang memiliki ukuran constraints yang salah sehingga memicu kalkulasi ulang ukuran yang mahal.
Kita juga dapat menampilkan grafik kinerja visual langsung di layar perangkat kita selama masa pengembangan dengan mengaktifkan parameter showPerformanceOverlay pada kelas MaterialApp kita:
// lib/main.dart
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
// Aktifkan overlay visual grafik kinerja hanya pada mode debug/development
showPerformanceOverlay: kDebugMode,
// Membantu mendeteksi apakah ada gambar yang dicache dengan ukuran salah
checkerboardRasterCacheImages: kDebugMode,
home: const HomeScreen(),
);
}
}
Membaca grafik kinerja overlay sangat sederhana: grafik atas mewakili durasi GPU (Raster thread) dan grafik bawah mewakili durasi CPU (UI thread). Jika grafik melompati garis batas horizontal merah, berarti terjadi jank pada frame tersebut.
Ringkasan #
- Prasyarat Profile: Selalu lakukan aktivitas profiling menggunakan Profile Mode (
flutter run --profile) di atas perangkat fisik asli. Hindari emulator atau Debug Mode karena overhead-nya sangat besar.- Penyaringan Thread: Gunakan tab Performance untuk membedakan antara masalah di UI Thread (logika Dart, rebuild) dengan masalah di Raster Thread (GPU, shader, asset gambar besar).
- Akar Masalah CPU: Saat menganalisis Flame Chart CPU, fokuskan pencarian pada fungsi dengan nilai
Self Timeyang tinggi untuk menemukan bottleneck sesungguhnya.- Marker Kustom: Sisipkan marker
developer.Timeline.startSync()untuk menandai alur fungsi buatan kita agar mudah diidentifikasi di dalam DevTools.- Deteksi Kebocoran: Gunakan perbandingan Heap Snapshot Diff di tab Memory untuk mencari objek yang tidak terhapus setelah halaman di-pop (delta bernilai positif).
- Disiplin Pembersihan: Cegah kebocoran memori dengan selalu memanggil metode
cancel()pada stream subscription dandispose()pada semua jenis controller.- Visualisasi Rebuild: Manfaatkan fitur Highlight Repaints di Widget Inspector untuk mendeteksi area gambar ulang yang terlalu luas dan memicu pemborosan GPU.
← Sebelumnya: Testing Best Practice Berikutnya: Rendering Optimization →