Memory & App Size #
Dua metrik yang sering diabaikan developer Flutter: berapa banyak RAM yang dikonsumsi app saat berjalan, dan seberapa besar file APK/IPA yang harus diunduh pengguna. App yang memakan terlalu banyak memori akan di-kill oleh OS, terutama di perangkat low-end. App yang terlalu besar membuat pengguna berpikir dua kali sebelum mengunduh — atau menghapusnya untuk menghemat ruang.
Sumber Memory Leak yang Umum #
Memory leak terjadi ketika objek yang sudah tidak dibutuhkan masih dipertahankan di memori karena masih ada referensi aktif yang menunjuk kepadanya.
// LEAK 1: AnimationController tidak di-dispose
class _ProductCardState extends State<ProductCard>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 300));
_controller.forward();
// LUPA dispose -- controller terus hidup meski widget sudah dihancurkan
}
@override
void dispose() {
_controller.dispose(); // WAJIB!
super.dispose();
}
}
// LEAK 2: StreamSubscription tidak dibatalkan
class DataWidget extends StatefulWidget { ... }
class _DataWidgetState extends State<DataWidget> {
StreamSubscription? _sub;
@override
void initState() {
super.initState();
_sub = dataService.stream.listen((data) {
setState(() { /* update UI */ });
});
}
@override
void dispose() {
_sub?.cancel(); // WAJIB! Tanpa ini, callback masih aktif meski widget sudah hilang
super.dispose();
}
}
// LEAK 3: Timer tidak dibatalkan
class _CountdownState extends State<Countdown> {
Timer? _timer;
@override
void initState() {
super.initState();
_timer = Timer.periodic(const Duration(seconds: 1), (t) {
setState(() { _seconds--; });
});
}
@override
void dispose() {
_timer?.cancel(); // WAJIB!
super.dispose();
}
}
// LEAK 4: ChangeNotifier listener tidak di-remove
class _ScreenState extends State<Screen> {
@override
void initState() {
super.initState();
widget.notifier.addListener(_onChanged);
}
void _onChanged() => setState(() {});
@override
void dispose() {
widget.notifier.removeListener(_onChanged); // WAJIB!
super.dispose();
}
}
Manajemen Gambar #
Gambar adalah kontributor terbesar konsumsi memori di kebanyakan app mobile.
// MASALAH: Gambar network yang tidak di-cache atau terlalu besar
Image.network('https://example.com/gambar-besar-4k.jpg')
// Gambar 4K dimuat penuh meskipun ditampilkan sebesar 100x100 pixel!
// SOLUSI 1: Tentukan cacheWidth/cacheHeight
Image.network(
'https://example.com/gambar-besar.jpg',
cacheWidth: 300, // resize saat di-decode, hemat memori
cacheHeight: 300,
fit: BoxFit.cover,
)
// SOLUSI 2: Gunakan package cached_network_image
// dependencies: cached_network_image: ^3.4.1
CachedNetworkImage(
imageUrl: produk.imageUrl,
width: 150,
height: 150,
memCacheWidth: 300, // resolusi di memory cache (2x untuk layar Retina)
memCacheHeight: 300,
placeholder: (ctx, url) => const Shimmer(),
errorWidget: (ctx, url, e) => const Icon(Icons.broken_image),
)
// SOLUSI 3: Untuk gambar lokal (asset), gunakan cacheWidth
Image.asset(
'assets/images/banner.png',
cacheWidth: 800, // tidak perlu load full resolution jika ditampilkan kecil
)
// SOLUSI 4: Preload gambar penting sebelum dipakai
@override
void didChangeDependencies() {
super.didChangeDependencies();
// Preload di background saat widget pertama kali masuk tree
precacheImage(const AssetImage('assets/images/splash.png'), context);
precacheImage(NetworkImage(produk.imageUrl), context);
}
// Bersihkan image cache jika memori menipis (opsional)
void clearImageCache() {
imageCache.clear();
imageCache.clearLiveImages();
}
Compute — Berat ke Isolate #
Operasi CPU-intensif di main isolate akan memblokir UI thread dan menyebabkan jank:
import 'dart:isolate';
import 'package:flutter/foundation.dart';
// ANTI-PATTERN: parsing JSON besar di main isolate
Future<List<Produk>> parseProdukBesar(String jsonString) async {
// Ini berjalan di main isolate -- UI freeze selama parsing!
final data = jsonDecode(jsonString) as List;
return data.map((e) => Produk.fromJson(e)).toList();
}
// BENAR: gunakan compute() untuk operasi berat
// compute() menjalankan fungsi di isolate terpisah
Future<List<Produk>> parseProdukBesar(String jsonString) async {
return compute(_parseProdukIsolate, jsonString);
}
// Fungsi ini harus top-level atau static (tidak bisa closure)
List<Produk> _parseProdukIsolate(String jsonString) {
final data = jsonDecode(jsonString) as List;
return data.map((e) => Produk.fromJson(e as Map<String, dynamic>)).toList();
}
// Contoh penggunaan
final produkList = await parseProdukBesar(responseBody);
// Untuk operasi yang lebih kompleks -- Isolate manual
Future<void> prosesDataBesar(List<Map<String, dynamic>> rawData) async {
final receivePort = ReceivePort();
await Isolate.spawn(
_prosesDataIsolate,
_IsolateParams(sendPort: receivePort.sendPort, data: rawData),
);
final hasil = await receivePort.first as List<Produk>;
// gunakan hasil
}
class _IsolateParams {
final SendPort sendPort;
final List<Map<String, dynamic>> data;
_IsolateParams({required this.sendPort, required this.data});
}
void _prosesDataIsolate(_IsolateParams params) {
final hasil = params.data
.where((e) => e['tersedia'] == true)
.map((e) => Produk.fromJson(e))
.toList()
..sort((a, b) => a.nama.compareTo(b.nama));
params.sendPort.send(hasil);
}
Optimasi Ukuran APK/IPA #
Analisis Ukuran App #
# Build dengan analisis ukuran
flutter build apk --analyze-size
flutter build appbundle --analyze-size
flutter build ios --analyze-size
# Output: *-code-size-analysis_*.json
# Buka di DevTools: flutter pub global run devtools
# Pilih "App Size" tab dan upload file JSON
# Atau gunakan --split-debug-info untuk reduce size
flutter build apk --release --split-debug-info=./debug-info
Tree Shaking dan Minifikasi #
# Flutter otomatis melakukan tree shaking saat build release
# Kode Dart yang tidak digunakan dihapus secara otomatis
# Pastikan tidak ada impor yang tidak digunakan
# Aktifkan lint: unused_import, unused_local_variable
# Untuk lebih agresif -- proguard di Android:
# android/app/proguard-rules.pro
// android/app/build.gradle
buildTypes {
release {
signingConfig signingConfigs.release
minifyEnabled true // aktifkan minifikasi
shrinkResources true // hapus resource yang tidak dipakai
proguardFiles getDefaultProguardFile('proguard-android.txt'),
'proguard-rules.pro'
}
}
Split APK per ABI #
# Buat APK terpisah per arsitektur CPU -- lebih kecil untuk setiap device
flutter build apk --split-per-abi
# Output:
# app-arm64-v8a-release.apk (~30MB) -- untuk device 64-bit modern
# app-armeabi-v7a-release.apk (~25MB) -- untuk device 32-bit
# app-x86_64-release.apk -- untuk emulator
# Untuk distribusi Play Store -- gunakan App Bundle (lebih baik dari split APK)
flutter build appbundle --release
# Play Store otomatis kirim ukuran yang tepat ke setiap device
Optimasi Asset #
# pubspec.yaml -- hanya include asset yang dibutuhkan
flutter:
assets:
- assets/images/ # jangan include folder besar jika tidak semua dipakai
- assets/images/logo.png # lebih baik: specify per file
- assets/images/banner.webp
fonts:
- family: Roboto
fonts:
- asset: assets/fonts/Roboto-Regular.ttf
- asset: assets/fonts/Roboto-Bold.ttf
# Jangan include weight yang tidak dipakai!
# Konversi PNG ke WebP untuk gambar yang lebih kecil
# WebP ~25-35% lebih kecil dari PNG/JPEG dengan kualitas sama
cwebp -q 85 input.png -o output.webp
# Untuk icon -- gunakan SVG via flutter_svg atau Vector Drawable
# Satu file SVG vs multiple PNG di berbagai resolusi
Deferred Loading (Lazy Import) #
// Untuk app besar -- load library hanya saat dibutuhkan
// Mengurangi ukuran initial download
import 'package:flutter/foundation.dart';
// Deferred import -- library tidak dimuat saat startup
import 'package:expensive_chart_library/charts.dart' deferred as charts;
class ReportScreen extends StatefulWidget { ... }
class _ReportScreenState extends State<ReportScreen> {
bool _isLoaded = false;
@override
void initState() {
super.initState();
_loadLibrary();
}
Future<void> _loadLibrary() async {
await charts.loadLibrary(); // download dan load saat diperlukan
setState(() => _isLoaded = true);
}
@override
Widget build(BuildContext context) {
if (!_isLoaded) return const CircularProgressIndicator();
return charts.ChartWidget(data: reportData);
}
}
Ringkasan #
- Memory leak paling umum:
AnimationController,StreamSubscription,Timer, danChangeNotifierlistener yang tidak di-dispose. Buat checklistdispose()untuk setiap resource yang dibuat diinitState().- Gambar adalah kontributor memori terbesar. Selalu tentukan
cacheWidth/cacheHeightsaat load gambar yang lebih besar dari yang ditampilkan. Gunakancached_network_imageuntuk gambar dari network.- Operasi CPU-intensif (parsing JSON besar, sorting, enkripsi) harus dijalankan di isolate terpisah menggunakan
compute()— jangan blokir UI thread.- Gunakan
flutter build apk --analyze-sizelalu buka di DevTools App Size tab untuk melihat apa yang paling banyak makan tempat di APK kamu.- App Bundle (
.aab) untuk Play Store lebih baik dari APK split — Google Play otomatis kirim ukuran yang tepat ke setiap device.- Konversi gambar ke WebP untuk ukuran 25–35% lebih kecil. Untuk icon, gunakan SVG via
flutter_svg.- Deferred loading menunda load library besar hingga benar-benar dibutuhkan — mengurangi ukuran initial download tapi menambah latensi saat pertama kali fitur dibuka.
← Sebelumnya: Rendering Optimization Berikutnya: Performance Best Practice →