Sitemap

Flutter — Widget Optimization

4 min readNov 25, 2025

--

Press enter or click to view image in full size

If you’re shipping a Flutter app, you definitely care about these things: smooth rendering (no dropped frames), fast screen opening time, and good user retention.

But most developers ignore one small yet crucial detail: how your app allocates and frees memory.

If you ignore this, you’ll keep chasing superficial “fake optimizations” (like spamming setState or inventing little tricks), yet your UI will still stutter.

If you understand it, you can eliminate jank, avoid memory leaks, and confidently ship your app.

Flutter’s widgets are simply immutable descriptions of the UI — think of them as design blueprints.

The heavy, long-lived objects live deeper in the Element and RenderObject layers.

Flutter reuses these underlying objects, so even if you rebuild the UI frequently, your memory will not blow up.

1.❓ Test

Q: Which uses more memory: StatelessWidget or StatefulWidget?

A: StatefulWidget uses more memory, because it introduces a State object that stays attached to the element tree.

The State object often holds controllers, listeners, data, and other references.

Try This :

  • Use const constructors whenever possible to avoid repeatedly creating identical widget objects.
  • Prefer StatelessWidget unless you truly need state. And when you do use StatefulWidget, be mindful about what you store inside the State.

2. 🔹 Rebuilds

If you want smoother frames, stop fearing rebuilds.

Dart’s garbage collector (GC) is generational:

  • Small, short-lived objects go into the new space, where a fast scavenger quickly cleans them up.
  • Long-lived objects get promoted to the old space, where a concurrent mark-sweep/compact cycle manages them.

Creating lots of short-lived widget objects is perfectly fine.

The real memory cost comes from large objects that get promoted and live a long time, or are created/destroyed repeatedly.

Examples:

  • Creating a small widget inside build() → cheap
  • Creating a list of 10k models or decoding a 4K image inside build() → expensive, likely promoted to old space

Try This:

Move heavy allocations off the hot path:

  • Precompute lists in initState
  • Use memoized selectors to avoid repeated expensive computations
  • Don’t decode huge images during rendering time

3. dispose()

Anything you create that listens, counts time, or holds a native resource must be disposed in dispose(). Includes:

  • AnimationController
  • ScrollController
  • TextEditingController
  • FocusNode
  • StreamSubscription
  • Timer
  • platform channels, etc.

Failing to dispose them causes memory leaks and dreaded errors like

Get Slime’s stories in your inbox

Join Medium for free to get updates from this writer.

“setState() called after dispose()”.

class ProfileScreenState extends State<ProfileScreen> {

final _controller = ScrollController();

StreamSubscription<User>? _sub;

@override
void initState() {
super.initState();
_sub = userStream.listen((u) => setState(() => _user = u));
}

@override
void dispose() {
_sub?.cancel();
_controller.dispose();
super.dispose();
}

}

An additional protection against (asynchronous / non-synchronous) updates

if ( ! mounted) return; // before setState after an await

4. Images: The Hidden Memory Hog

Images consume huge amounts of memory, and most people ignore them.

Flutter’s ImageCache uses an LRU strategy.

By default:

  • Max 1,000 images
  • Approx 100 MB compressed size

⚠️ Note: The 100 MB refers to compressed cache.

Once decoded to bitmaps, images are much larger.

Example:

A 7500×5000 px image → over 100 MB decoded.

It can easily exceed your memory budget and force re-decodes or jank.

Try This :

  • Provide cacheWidth or cacheHeight when loading images. This lets Flutter decode at the actual size your UI needs, not full resolution.
  • If your app uses many images, tune the ImageCache limits.
Image.network(url, cacheWidth: 1080)

void main() {
PaintingBinding.instance.imageCache.maximumSizeBytes = 80 * 1024 * 1024;
runApp(const MyApp());
}

5. Lists and Memory Optimization

  1. Use ListView.builder over ListView( children: […] ).

The builder version only builds what’s visible, saving huge memory.

2. Avoid abusing AutomaticKeepAliveClientMixin.

It pins off-screen widgets in memory, preventing them from being freed.

6. Keys: Prevent Unnecessary Rebuilds

A Key prevents accidental “discard and rebuild” of elements.

Without the correct key, Flutter may discard the element and rebuild state you intended to preserve.

Use the right key to reduce allocations, preserve scroll position, and avoid controller re-binding.

Recommendations:

  • Use ValueKey for stable IDs
  • Use PageStorageKey to preserve scroll position
  • Do NOT create random keys during every build

Common Issues

  • Doing heavy work inside build(): It runs far more frequently than you think. Precompute or memoize instead.
  • Decoding images at full resolution: Always decode to the pixel size you actually need.
  • Forgetting to release “invisible” objects: StreamSubscription and Timer leaks are just as dangerous as controller leaks.

Performance Analysis

  1. Open DevTools → Memory: Navigate your laggiest screen for 30 seconds. Look for rising baselines or sawtooth patterns.
  2. Search your codebase for objects requiring disposal: AnimationController|ScrollController|TextEditingController|FocusNode|StreamSubscription|Timer, ensure each has a corresponding dispose() or cancel().
  3. Add cacheWidth/cacheHeight to your largest images. Adjust ImageCache.maximumSizeBytes if needed.
  4. Replace massive ListView( children: […] ) with ListView.builder.
  5. Add const to stable subtrees and fix missing keys where identity must stay stable.

App Checklist

  1. In profile mode, capture 30–60 seconds on two of your slowest screens, and record memory peaks, GC frequency, and jank percentage.
  2. Use leak_tracker with unit/widget tests for modules like chat or media. Fail the test if controllers or subscriptions survive after dispose.
  3. Examine your top 10 largest images/thumbnails. Add decoding size limits and adjust cache size. Re-measure performance afterward.

--

--

Slime
Slime

Written by Slime

If you enjoy my articles, consider buying me a coffee to support my work! buymeacoffee.com/slime8992