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, dan ChangeNotifier listener yang tidak di-dispose. Buat checklist dispose() untuk setiap resource yang dibuat di initState().
  • Gambar adalah kontributor memori terbesar. Selalu tentukan cacheWidth/cacheHeight saat load gambar yang lebih besar dari yang ditampilkan. Gunakan cached_network_image untuk 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-size lalu 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 →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact