Published on

Flutter Performance Part 4: Detoxing RAM & Shedding App Size

Authors
  • avatar
    Name
    Phat Tran
    Twitter

Your banking app might be running at 60 FPS on the main thread, but if it suddenly crashes back to the home screen after 10 minutes of use, you have a completely different problem: an Out-Of-Memory (OOM) crash.

Mobile operating systems are ruthless. If an app consumes too much RAM, iOS and Android will silently assassinate it in the background to protect the system.

In Part 4, we will learn how to "detox" our RAM and put the final installation size of our app on a strict diet.


1. The Number One Culprit: Image Caching

The single most common cause of OOM crashes in Flutter apps is mishandling network images.

Imagine your banking app has a social feature where users can see their friends' avatars in a 50x50 pixel circle. If the backend serves 4K resolution images, and you use a standard Image.network(url), Flutter will download the 4K image and decode the entire massive bitmap into RAM, just to display it in a tiny 50x50 box. Multiply this by a list of 100 friends, and your app will instantly crash.

The Solution: Always resize images at the decode level.

// ❌ BAD: Decodes the full 4K image into RAM.
CircleAvatar(
  radius: 25,
  backgroundImage: NetworkImage(user.avatarUrl),
);

// ✅ GOOD: Forces the Engine to decode the image into a tiny, memory-friendly bitmap.
CircleAvatar(
  radius: 25,
  backgroundImage: ResizeImage(
    NetworkImage(user.avatarUrl),
    width: 100, // Roughly 2x the radius for high-DPI screens
    height: 100,
  ),
);

By explicitly setting cacheWidth or cacheHeight (or using ResizeImage), you prevent hundreds of megabytes of wasted RAM.

2. Plugging Memory Leaks

A memory leak occurs when you create an object in RAM, stop using it, but the Garbage Collector (GC) cannot delete it because something else is still holding a reference to it.

In Flutter, this almost always happens when you forget to clean up after yourself.

  • Controllers: Always call dispose() on TextEditingController, ScrollController, and AnimationController inside your widget's dispose method.
  • Streams & Timers: If you start a Timer.periodic or listen to a RxDart BehaviorSubject, you must cancel the subscription when the user leaves the screen.
  • The Async Gap: If you make a 5-second API call, but the user presses the "Back" button after 2 seconds, the widget is destroyed. When the API call finishes, your code might attempt to update the UI of a destroyed widget, causing an exception or a memory leak.
// ✅ GOOD: Always check if the widget still exists after an async gap.
Future<void> fetchBalance() async {
  final balance = await api.getBalance();

  // If the user left the screen, do not update the state!
  if (!context.mounted) return;

  setState(() {
    _balance = balance;
  });
}

3. Shedding App Size

Users hate downloading 150MB apps just to check their bank balance. A bloated app size decreases conversion rates and takes up unnecessary space on the user's phone.

Before shipping your app to production, you should always run an analysis to see exactly what is making your app heavy.

# Analyze your Android APK
flutter build apk --analyze-size

# Analyze your iOS App
flutter build ipa --analyze-size

Running this command will generate a detailed interactive JSON map. You can open it in Flutter DevTools and visually inspect your bundle.

  • Are you shipping high-resolution .png onboarding assets that should be converted to .webp?
  • Did you accidentally bundle an entire 10MB custom font file when you only use the "Bold" and "Regular" weights?
  • Is there a heavy third-party package dragging down your binary size?

Cut the dead weight, optimize your assets, and keep your app lean.


Up Next: Breaking the Speed Limit with Rust

We’ve optimized our Dart code, our memory, and our architecture. But what if Dart itself isn't fast enough? What if you need to execute raw C/Rust speed? In our final Part 5, we will explore the extreme edge of performance: Zero-overhead Native FFI.