Best Practice #
Optimasi performa yang efektif selalu didasarkan pada data konkret dan pengukuran objektif, bukan pada asumsi atau intuisi semata. Dalam pengembangan aplikasi Flutter, kode yang secara visual terlihat “sederhana” atau “bersih” belum tentu efisien di tingkat eksekusi mesin. Sebaliknya, bagian kode yang kita curigai sebagai penyebab utama kelambatan sering kali bukanlah bottleneck yang sesungguhnya. Tanpa metodologi pengukuran yang tepat, kita berisiko membuang waktu berharga untuk mengoptimalkan bagian aplikasi yang salah, yang pada akhirnya tidak memberikan dampak signifikan bagi pengalaman pengguna akhir.
Dalam artikel penutup Section 10 ini, kita akan merangkum prinsip-prinsip desain terbaik, metodologi benchmarking, alur pengambilan keputusan optimasi, serta anti-pattern yang harus kita hindari. Tujuannya adalah membekali kita dengan kerangka berpikir yang sistematis dalam memelihara dan meningkatkan kinerja aplikasi Flutter pada skala produksi.
Metodologi: Profiling Sebelum Optimasi #
Prinsip fundamental pertama dalam rekayasa performa perangkat lunak adalah: Jangan pernah mengoptimalkan kode tanpa data profiling. Upaya optimasi yang dilakukan secara spekulatif (menebak-nebak) sering kali berujung pada peningkatan kompleksitas kode tanpa adanya perbaikan kinerja yang nyata, atau bahkan dapat memperkenalkan bug baru.
Untuk mengidentifikasi bagian kode yang lambat secara akurat, kita dapat mengintegrasikan pengujian kinerja manual menggunakan instansi Stopwatch bawaan Dart atau mengotomatisasikannya ke dalam bentuk fungsi utility benchmark yang dapat digunakan berulang kali.
// Implementasi utility benchmark untuk mengukur rata-rata waktu eksekusi
import 'package:flutter/foundation.dart';
Future<void> benchmarkOperasi({
required String label,
required Future<void> Function() operasi,
int jumlahIterasi = 10,
int jumlahWarmUp = 2,
}) async {
// 1. Fase Warm-up: Memastikan JIT compiler Dart telah mengompilasi kode ke machine code
for (var i = 0; i < jumlahWarmUp; i++) {
await operasi();
}
final daftarWaktu = <int>[];
// 2. Fase Pengukuran Aktual
for (var i = 0; i < jumlahIterasi; i++) {
final stopwatch = Stopwatch()..start();
await operasi();
stopwatch.stop();
daftarWaktu.add(stopwatch.elapsedMilliseconds);
}
// 3. Kalkulasi Statistik Sederhana
final totalWaktu = daftarWaktu.reduce((a, b) => a + b);
final rataRata = totalWaktu / jumlahIterasi;
final nilaiTerkecil = daftarWaktu.reduce((a, b) => a < b ? a : b);
final nilaiTerbesar = daftarWaktu.reduce((a, b) => a > b ? a : b);
debugPrint('--- HASIL BENCHMARK: $label ---');
debugPrint('Rata-rata Eksekusi : ${rataRata.toStringAsFixed(2)} ms');
debugPrint('Rentang Waktu : $nilaiTerkecil ms - $nilaiTerbesar ms');
debugPrint('Detail Iterasi : ${daftarWaktu.join(", ")} ms\n');
}
// Contoh Penggunaan dalam Kode Aplikasi kita:
void jalankanPengujian() async {
await benchmarkOperasi(
label: 'Parsing Data JSON JSON-Placeholder',
operasi: () async {
// Masukkan fungsi yang ingin kita uji kinerjanya di sini
await memprosesJsonPayloadLokal();
},
jumlahIterasi: 15,
);
}
Future<void> memprosesJsonPayloadLokal() async {
// Simulasi kalkulasi data
await Future.delayed(const Duration(milliseconds: 12));
}
Meskipun instrumen Stopwatch sangat baik untuk pengujian terisolasi pada fungsi tertentu, untuk analisis performa aplikasi secara menyeluruh, kita harus mengandalkan Flutter DevTools CPU Profiler dan Performance Timeline. Alat-alat ini memberikan wawasan mendalam mengenai tumpukan panggilan metode (call stacks), durasi frame rendering, dan penggunaan memori secara real-time saat aplikasi dijalankan dalam mode Profile.
Menghindari Komputasi Berat di Metode Build #
Metode build() di dalam widget Flutter dirancang untuk memiliki satu tanggung jawab utama: mendeklarasikan konfigurasi antarmuka pengguna secara deklaratif. Engine Flutter akan memanggil metode build() ini secara sangat sering dan berulang-ulang—seperti saat terjadi animasi, transisi halaman, pembukaan keyboard input, atau perubahan status state lokal.
Oleh karena itu, menempatkan operasi komputasi berat, manipulasi data kompleks, sorting list, atau parsing JSON langsung di dalam metode build() adalah anti-pattern yang sangat fatal. Setiap kali widget mengalami rebuild, komputasi berat tersebut akan dieksekusi ulang dari awal, yang secara instan akan memblokir UI thread dan menyebabkan penurunan frame secara drastis (dropped frames).
// ANTI-PATTERN: Melakukan manipulasi data berat langsung di dalam metode build()
class DaftarProdukBocorScreen extends StatelessWidget {
final List<Map<String, dynamic>> rawData;
const DaftarProdukBocorScreen({super.key, required this.rawData});
@override
Widget build(BuildContext context) {
// Operasi ini dijalankan ulang SETIAP KALI widget ini atau parent-nya rebuild!
final daftarProduk = rawData
.map((item) => ProdukModel.fromJson(item)) // parsing berat
.where((produk) => produk.stok > 0) // filter data
.toList()
..sort((a, b) => a.nama.compareTo(b.nama)); // sorting list
return ListView.builder(
itemCount: daftarProduk.length,
itemBuilder: (context, index) {
return ListTile(title: Text(daftarProduk[index].nama));
},
);
}
}
// SOLUSI: Lakukan komputasi di luar build(), misalnya pada State Management / Notifier
class DaftarProdukAmanScreen extends ConsumerWidget {
const DaftarProdukAmanScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// Kita hanya mengamati hasil akhir data yang sudah diproses di tingkat provider
final stateProduk = ref.watch(produkTersortirProvider);
return stateProduk.when(
data: (daftarProduk) => ListView.builder(
itemCount: daftarProduk.length,
itemBuilder: (context, index) {
return ListTile(title: Text(daftarProduk[index].nama));
},
),
loading: () => const Center(child: CircularProgressIndicator()),
error: (err, stack) => Center(child: Text('Terjadi kesalahan: $err')),
);
}
}
Dengan memindahkan logika manipulasi data ke layer bisnis (seperti Riverpod Provider, Bloc, atau ChangeNotifier), kita memastikan bahwa proses kalkulasi hanya berjalan satu kali ketika data sumber berubah, dan metode build() hanya bertugas merender output yang sudah matang secara instan.
Penggunaan Key yang Tepat untuk Efisiensi Widget #
Untuk memahami mengapa penggunaan Key sangat krusial bagi performa aplikasi, kita harus mengingat kembali arsitektur internal Flutter yang mengelola tiga pohon struktur objek (Three Trees): Widget Tree, Element Tree, dan RenderObject Tree.
Widget bersifat tidak kekal (immutable) dan murah untuk dihancurkan serta dibuat ulang. Namun, Element (yang merepresentasikan instansi state yang hidup) dan RenderObject (yang menangani tata letak dan gambar aktual) bersifat persisten dan sangat mahal untuk dibuat dari awal.
Ketika sebuah widget baru digantikan dengan widget lama, Flutter menggunakan algoritma rekonsiliasi yang membandingkan tipe dan kunci widget:
$$\text{Apakah } (\text{widget.runtimeType} == \text{oldWidget.runtimeType}) \text{ dan } (\text{widget.key} == \text{oldWidget.key}) \text{ ?}$$
Jika hasilnya true, Flutter hanya akan memperbarui konfigurasi RenderObject yang sudah ada tanpa menghancurkannya. Namun, jika kita memanipulasi list widget yang dinamis (seperti menambah, menghapus, atau mengurutkan ulang item dalam sebuah list scrollable) tanpa memberikan kunci unik yang stabil, Flutter dapat keliru mencocokkan widget dengan element tree. Hal ini mengakibatkan hilangnya status state internal widget atau memaksa Flutter menghancurkan dan membangun ulang seluruh element beserta render object dari awal, yang memakan waktu komputasi besar.
// ANTI-PATTERN: List dinamis yang dapat diurutkan ulang tanpa menggunakan Key yang stabil
ListView.builder(
itemCount: daftarTugas.length,
itemBuilder: (context, index) {
// Tanpa Key, Flutter kesulitan mengidentifikasi perpindahan item secara efisien
return TugasItemWidget(tugas: daftarTugas[index]);
},
)
// SOLUSI: Berikan ValueKey yang diikat ke ID unik dan stabil dari model data kita
ListView.builder(
itemCount: daftarTugas.length,
itemBuilder: (context, index) {
final item = daftarTugas[index];
return TugasItemWidget(
key: ValueKey(item.id), // ID unik stabil, jangan gunakan index sebagai key!
tugas: item,
);
},
)
[!WARNING] Hindari menggunakan nomor indeks list (misalnya
ValueKey(index)) sebagai parameter kunci widget. Indeks bersifat dinamis dan akan berubah ketika item di atasnya dihapus atau disisipkan, yang akan merusak logika pelacakan element oleh Flutter dan memicu pembuatan ulang RenderObject yang tidak perlu.
Mempersempit Cakupan State dengan Lokalisasi setState #
Ketika kita memanggil metode setState() pada sebuah objek State, Flutter akan menandai elemen widget tersebut sebagai dirty (kotor) dan menjadwalkannya untuk dibangun ulang pada frame berikutnya. Proses rebuild ini bersifat kaskade: widget yang memanggil setState() beserta seluruh widget anak di bawahnya (descendants) akan dipanggil metode build()-nya secara rekursif.
Jika kita menempatkan setState() di tingkat root halaman utama yang berisi banyak komponen visual statis yang kompleks (seperti grafik, peta, atau list gambar panjang), seluruh halaman tersebut akan dirender ulang hanya karena perubahan kecil pada satu elemen teks. Hal ini merupakan pemborosan resource CPU yang sangat besar.
// ANTI-PATTERN: Memanggil setState di root widget untuk perubahan lokal
class DashboardBocorScreen extends StatefulWidget {
const DashboardBocorScreen({super.key});
@override
State<DashboardBocorScreen> createState() => _DashboardBocorScreenState();
}
class _DashboardBocorScreenState extends State<DashboardBocorScreen> {
int _jumlahKlik = 0;
@override
Widget build(BuildContext context) {
// Saat _jumlahKlik berubah, seluruh struktur widget ini akan dibangun ulang!
return Scaffold(
appBar: AppBar(title: const Text('Dashboard')),
body: Column(
children: [
const KomponenGrafikSangatBerat(), // Rebuild tidak berguna!
const PetaLokasiStatis(), // Rebuild tidak berguna!
Text('Jumlah Klik: $_jumlahKlik'),
ElevatedButton(
onPressed: () => setState(() => _jumlahKlik++),
child: const Text('Tambah'),
),
],
),
);
}
}
// SOLUSI: Lokalisasikan State dengan mengekstrak widget kecil yang dinamis
class DashboardAmanScreen extends StatelessWidget {
const DashboardAmanScreen({super.key});
@override
Widget build(BuildContext context) {
// Widget ini sekarang statis dan tidak akan pernah rebuild secara tidak perlu
return Scaffold(
appBar: AppBar(title: const Text('Dashboard')),
body: const Column(
children: [
KomponenGrafikSangatBerat(),
PetaLokasiStatis(),
TombolKonterDinamis(), // Hanya widget kecil ini yang akan rebuild
],
),
);
}
}
class TombolKonterDinamis extends StatefulWidget {
const TombolKonterDinamis({super.key});
@override
State<TombolKonterDinamis> createState() => _TombolKonterDinamisState();
}
class _TombolKonterDinamisState extends State<TombolKonterDinamis> {
int _jumlahKlik = 0;
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('Jumlah Klik: $_jumlahKlik'),
ElevatedButton(
onPressed: () => setState(() => _jumlahKlik++),
child: const Text('Tambah'),
),
],
);
}
}
Sebagai alternatif selain mengekstrak widget secara manual, kita juga dapat memanfaatkan widget bawaan Flutter seperti ValueListenableBuilder atau state manager dengan fitur seleksi granular seperti select() pada Riverpod untuk meminimalkan cakupan rebuild secara deklaratif.
Mengurangi Alokasi Objek Berulang (Garbage Collection Reduction) #
Sebagaimana dibahas di bagian manajemen memori, setiap objek yang dialokasikan di dalam memori heap Dart VM pada akhirnya harus dibersihkan oleh Garbage Collector. Jika aplikasi kita melakukan alokasi ribuan objek baru berumur pendek setiap beberapa milidetik (misalnya saat mendeteksi pergerakan scroll list atau selama transisi animasi), Garbage Collector akan bekerja sangat keras (GC Thrashing). Jeda pendek yang disebabkan oleh pengumpulan sampah ini dapat memicu kelambatan UI yang sangat mengganggu kenyamanan pengguna.
Salah satu cara paling efektif untuk mereduksi beban kerja GC adalah dengan memaksimalkan penggunaan konstanta (const).
// ANTI-PATTERN: Mengalokasikan instansi objek baru setiap kali metode build dipanggil
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(16.0), // Objek baru dialokasikan setiap rebuild!
decoration: BoxDecoration( // Objek baru dialokasikan setiap rebuild!
color: Colors.blue,
borderRadius: BorderRadius.circular(8.0),
),
child: Text(
'Konfigurasi Statis',
style: TextStyle(fontSize: 14, color: Colors.white), // Objek baru dialokasikan!
),
);
}
// SOLUSI: Gunakan keyword const untuk membuat instansi tunggal pada waktu kompilasi
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16.0), // Kompilator mengalokasikan objek ini hanya sekali
decoration: const BoxDecoration( // Kompilator mengalokasikan objek ini hanya sekali
color: Colors.blue,
borderRadius: BorderRadius.all(Radius.circular(8.0)),
),
child: const Text(
'Konfigurasi Statis',
style: TextStyle(fontSize: 14, color: Colors.white), // Reusable instance
),
);
}
Ketika kita menambahkan keyword const di depan konstruktor widget, Dart akan membuat instansi objek tersebut satu kali saja pada saat kompilasi (compile-time constant) dan menyimpannya di dalam tabel memori statis. Saat widget tersebut perlu dirender ulang, Flutter akan menggunakan instansi yang sama secara berulang kali tanpa melakukan alokasi memori baru di heap, sehingga meringankan beban GC secara signifikan.
Mempertahankan State Tab dengan AutomaticKeepAlive #
Dalam layout bertingkat seperti penggunaan TabBarView atau PageView, Flutter secara default mengadopsi kebijakan hemat memori: widget halaman tab yang sedang tidak aktif atau tergeser keluar dari layar akan dihancurkan dari memori secara otomatis. Ketika pengguna menggeser kembali ke tab tersebut, halaman tersebut akan dibangun ulang dari awal.
Meskipun menghemat RAM dalam jangka pendek, perilaku default ini berdampak sangat buruk bagi performa dan kenyamanan pengguna (User Experience) karena:
- Halaman harus memanggil ulang API request dari internet setiap kali berpindah tab.
- Pengguna kehilangan posisi scroll terakhir mereka pada tab tersebut.
- Layar menampilkan indikator pemuatan (loading indicator) secara berulang-ulang.
Untuk mempertahankan state halaman tab agar tidak dihancurkan tanpa mengorbankan performa, kita harus menggunakan mixin AutomaticKeepAliveClientMixin pada state widget halaman tab kita.
// Implementasi penahanan state tab menggunakan AutomaticKeepAliveClientMixin
class HalamanTabProduk extends StatefulWidget {
const HalamanTabProduk({super.key});
@override
State<HalamanTabProduk> createState() => _HalamanTabProdukState();
}
// 1. Tambahkan mixin AutomaticKeepAliveClientMixin ke State kelas kita
class _HalamanTabProdukState extends State<HalamanTabProduk>
with AutomaticKeepAliveClientMixin {
// 2. Override getter wantKeepAlive dan kembalikan nilai true
@override
bool get wantKeepAlive => true;
@override
void initState() {
super.initState();
_fetchDataSekaliSaja();
}
void _fetchDataSekaliSaja() {
// Mengambil data dari server
}
@override
Widget build(BuildContext context) {
// 3. WAJIB memanggil super.build(context) di baris pertama metode build
super.build(context);
return ListView.builder(
itemCount: 100,
itemBuilder: (context, index) {
return ListTile(
title: Text('Produk Kategori A - Ke-$index'),
);
},
);
}
}
Dengan mengaktifkan wantKeepAlive => true, Flutter akan mempertahankan elemen halaman tab tersebut tetap hidup di dalam memori heap latar belakang meskipun tidak terlihat di layar. Hal ini menjamin transisi perpindahan tab yang instan, memelihara posisi scroll pengguna, dan menghemat konsumsi kuota data internet akibat pemicuan request API berulang.
Alur Pengambilan Keputusan Optimasi #
Saat kita menghadapi masalah performa aplikasi, kita tidak boleh melakukan perubahan kode secara acak. Kita membutuhkan pendekatan yang terstruktur untuk mengidentifikasi penyebab masalah dan menentukan jenis optimasi yang tepat.
Gunakan flowchart berikut sebagai panduan sistematis untuk memprioritaskan keputusan optimasi kinerja aplikasi kita:
flowchart TD
Start([Mulai Analisis Performa]) --> Measure["Ukur dengan DevTools dalam Mode Profile"]
Measure --> CheckJank{"Apakah ada Frame Drop / Jank?"}
CheckJank -- Ya --> IdentifySource{"Apa penyebab Jank?"}
IdentifySource -- UI Thread Terblokir --> HeavyComp["Pindahkan Kalkulasi Berat ke Isolate"]
IdentifySource -- Engine Render Lambat --> OptimizeWidget["Optimasi Widget Tree & Kurangi Rebuild"]
IdentifySource -- GPU Bottleneck --> SaveLayer["Kurangi SaveLayer, Opacity, & Clip"]
CheckJank -- Tidak --> CheckMemory{"Apakah ada Kebocoran Memori / OOM?"}
CheckMemory -- Ya --> TraceLeaks["Gunakan Heap Snapshot untuk Melacak Leak"]
TraceLeaks --> DisposeLeaks["Dispose Controllers & Cancel Subscriptions"]
CheckMemory -- Tidak --> CheckSize{"Apakah Ukuran Bundel Terlalu Besar?"}
CheckSize -- Ya --> AnalyzeSize["Jalankan --analyze-size & Optimasi Aset"]
CheckSize -- Tidak --> KeepMonitoring["Pantau Performa Secara Berkala"]
HeavyComp --> Verify["Verifikasi Ulang Performa"]
OptimizeWidget --> Verify
SaveLayer --> Verify
DisposeLeaks --> Verify
AnalyzeSize --> Verify
Verify --> CheckJank
KeepMonitoring --> End([Selesai])Anti-Pattern Performa Umum di Flutter #
Berikut adalah daftar kompilasi kesalahan umum (anti-patterns) yang sering ditemui dalam pengembangan aplikasi Flutter skala menengah hingga besar, disertai alternatif solusi terbaiknya:
- Mengakses MediaQuery.of(context) di Tingkat Root Build:
- Mengapa bermasalah: Memanggil
MediaQuery.of(context)menyebabkan widget tersebut melakukan registrasi dependensi terhadap data orientasi atau dimensi layar global. Setiap kali ada perubahan kecil (seperti orientasi perangkat berputar, keyboard input muncul/tenggelam, atau perubahan kecerahan layar), widget root beserta seluruh anak di bawahnya akan dipaksa melakukan rebuild secara total. - Solusi: Gunakan properti spesifik seperti
MediaQuery.sizeOf(context)(tersedia pada versi Flutter modern) yang hanya memicu rebuild jika dimensi layar berubah, atau gunakan widget layout responsif sepertiLayoutBuilderuntuk membatasi cakupan pengaruh perubahan dimensi.
- Mengapa bermasalah: Memanggil
- Menggunakan Opacity Widget untuk Animasi Transparansi Statik:
- Mengapa bermasalah: Widget
Opacitymemaksa engine render Flutter untuk membuat buffer rendering di luar layar (offscreen buffer) melalui panggilansaveLayerpada GPU. Proses ini sangat mahal bagi kartu grafis perangkat seluler. - Solusi: Untuk transparansi warna statis, gunakan properti warna dengan opacity secara langsung (misalnya
Color.fromRGBO(0, 0, 0, 0.5)atauColors.black.withOpacity(0.5)). Untuk menyembunyikan widget sepenuhnya, gunakan widget kondisional Dart atau widgetVisibility.
- Mengapa bermasalah: Widget
- Melakukan Operasi Clip yang Tidak Diperlukan:
- Mengapa bermasalah: Operasi pemotongan kontainer seperti
ClipRRect,ClipOval, atau properticlipBehavior: Clip.antiAliasmemaksa GPU melakukan pemotongan piksel secara presisi yang membutuhkan siklus kalkulasi grafis ekstra. - Solusi: Gunakan properti borders pada
BoxDecorationatau bentuk gambar melengkung langsung dari aset asli untuk meminimalkan kebutuhan pemotongan di sisi engine aplikasi. GunakanclipBehavior: Clip.nonejika kontainer kita tidak berisiko meluap (overflow).
- Mengapa bermasalah: Operasi pemotongan kontainer seperti
- Membuat Widget via Fungsi Helper Dibandingkan Widget Class:
- Mengapa bermasalah: Membungkus potongan UI ke dalam fungsi biasa (misalnya
Widget _buildItem()) membuat Flutter tidak memiliki cara untuk melakukan optimasi kompilasi atau mendeteksi apakah bagian tersebut perlu dibangun ulang secara terisolasi. Seluruh bagian yang dihasilkan fungsi helper akan dirender ulang setiap kali widget utama rebuild. - Solusi: Selalu bungkus potongan UI kita ke dalam kelas widget mandiri yang diturunkan dari
StatelessWidgetatauStatefulWidget, serta gunakan konstruktorconstjika memungkinan.
- Mengapa bermasalah: Membungkus potongan UI ke dalam fungsi biasa (misalnya
Menentukan Kapan dan Apa yang Harus Dioptimasi #
Proses optimasi performa membutuhkan investasi waktu dan kompleksitas kode tambahan. Oleh karena itu, kita harus bijak dalam menentukan kapan waktu yang tepat untuk mulai melakukan optimasi.
JANGAN lakukan optimasi jika:
✗ Aplikasi kita belum memiliki fungsionalitas inti yang stabil.
✗ Kita belum menguji kinerja aplikasi pada perangkat fisik target (bukan emulator).
✗ Pengukuran DevTools menunjukkan frame rate aplikasi kita stabil di kisaran 60fps/120fps.
✗ Kompleksitas kode hasil optimasi melebihi manfaat peningkatan performa yang didapatkan.
MULAI lakukan optimasi jika:
✓ Hasil pengukuran Profile menunjukkan konsumsi RAM aplikasi naik terus tanpa batas (memory leak).
✓ Pengguna melaporkan kelambatan visual (jank) yang jelas pada halaman utama atau alur transaksi.
✓ File instalasi aplikasi (APK/IPA) melebihi batas psikologis target pasar kita (misalnya > 50 MB untuk pasar berkembang).
✓ Waktu inisialisasi awal aplikasi (cold start) memakan waktu lebih dari 3 detik di perangkat berspesifikasi rendah.
Dengan memfokuskan energi optimasi pada bagian-bagian kritis yang memberikan dampak langsung pada kenyamanan pengguna, kita dapat menjaga kode aplikasi kita tetap mudah dipelihara sekaligus mempertahankan performa yang optimal pada perangkat rilis.
Checklist Performa Sebelum Rilis #
Gunakan lembar kerja checklist di bawah ini sebagai langkah verifikasi terakhir sebelum kita mempublikasikan versi rilis aplikasi Flutter kita ke Google Play Store atau Apple App Store:
1. Rendering & Layout #
- Semua widget statis telah menggunakan konstruktor
constdi depannya. - Tidak ada widget
Opacitystatis yang dapat digantikan dengan pewarnaanwithOpacity(). - Semua scroll list yang panjang telah diimplementasikan menggunakan konstruktor builder (seperti
ListView.builderatauGridView.builder). - Cakupan pemanggilan
setState()telah diperkecil dengan melokalisasi state dinamis ke widget anak yang terpisah. - Penggunaan
MediaQuery.of(context)telah dioptimalkan atau digantikan denganMediaQuery.sizeOf(context)untuk mencegah rebuild massal yang tidak perlu.
2. Memori & Resource #
- Semua instansi
AnimationControllertelah dipanggil metodedispose()-nya pada fungsidispose()state. - Semua objek input controller (
TextEditingController,ScrollController,FocusNode) telah ditutup dengan benar di dalam metodedispose(). - Semua objek
StreamSubscriptiondan instansiTimeraktif telah dibatalkan (cancel()) sebelum widget dihancurkan. - Seluruh aset gambar yang berukuran besar telah dibatasi resolusi decoding-nya di RAM menggunakan parameter
cacheWidthataucacheHeight. - Aplikasi telah diuji menggunakan Memory Heap Snapshot pada Flutter DevTools dan terbukti bebas dari kebocoran memori pasca navigasi.
3. Ukuran Bundel Aplikasi (App Size) #
- Aplikasi Android dibuild menggunakan format Android App Bundle (
flutter build appbundle --release) untuk rilis Play Store. - Perintah build rilis telah menyertakan parameter
--obfuscatedan opsi--split-debug-infountuk mereduksi ukuran biner. - Seluruh aset gambar statis telah dikonversikan ke dalam format WebP terkompresi.
- Font bawaan aplikasi hanya menyertakan variasi ukuran (weight) yang digunakan di dalam aplikasi.
- Kita telah menjalankan perintah
flutter build --analyze-sizeuntuk memastikan tidak ada file mati yang tidak sengaja terikut di dalam bundel rilis.
4. Concurrency & Komputasi #
- Seluruh proses decoding payload JSON berukuran besar (> 1 MB) dari API internet telah dipindahkan dari Main Isolate menggunakan bantuan fungsi
compute()atau Isolate eksternal. - Operasi kriptografis berat atau manipulasi database lokal skala besar dijalankan di latar belakang tanpa memblokir thread rendering utama.
Ringkasan #
- Metodologi Profiling: Selalu ukur performa menggunakan perangkat fisik asli dalam mode Profile (
flutter run --profile) sebelum kita mulai mengubah kode. Jangan pernah melakukan optimasi hanya berdasarkan asumsi atau perkiraan subjektif.- Optimasi Metode Build: Jaga metode
build()agar tetap murni untuk deskripsi UI. Pindahkan seluruh kalkulasi kompleks, filter array, atau pengolahan data mentah ke layer bisnis (Notifier/Provider).- Stabilitas Element dengan Key: Gunakan
ValueKeyberbasis ID unik yang stabil ketika kita berurusan dengan list dinamis yang dapat berubah urutan, ditambah, atau dikurangi agar Flutter dapat memetakan element tree secara efisien.- Granularitas State: Pecah widget besar kita menjadi komponen-komponen kecil yang terisolasi untuk mempersempit cakupan rebuild yang dipicu oleh panggilan
setState().- Efisiensi GC: Manfaatkan keyword
constsemaksimal mungkin untuk menghindari alokasi objek berulang di memori heap, sehingga mengurangi frekuensi berjalannya Garbage Collector.- Penyimpanan State Tab: Terapkan
AutomaticKeepAliveClientMixinpada halaman tab agar data dan posisi scroll pengguna tidak hilang saat mereka berpindah-pindah tab.
← Sebelumnya: Memory & App Size Berikutnya: Platform Channels →