1. Eliminating Unnecessary Widget Tree Rebuilds
Unnecessary rebuilds are the #1 cause of UI frame drops in Flutter. Best practices include:
- Use `const` Constructors Aggressively: A `const` widget is instantiated once at compile time and skipped entirely by the reconciliation engine during state changes.
- Isolate State Scope: Never call `setState()` at the root of a large page. Extract animated widgets or inputs into small, isolated StatefulWidget/ConsumerWidget sub-trees.
- Selective Selectors: Use `ref.watch(provider.select((s) => s.property))` in Riverpod or `BlocSelector` in BLoC to trigger rebuilds ONLY when the specific observed property changes.
// Example: Selective rebuilding in Riverpod
final userName = ref.watch(userProvider.select((user) => user.name));
return Text(userName); // Will NOT rebuild if user.avatar or user.email changes!
2. RepaintBoundary and Custom Drawing Optimization
When a widget animates (e.g. a pulsing radar or loading spinner), Flutter repaints the entire RenderObject tree by default. Wrapping the animated subtree in a RepaintBoundary creates an isolated GPU render layer, preventing neighboring static widgets from repainting.
RepaintBoundary(
child: CustomPaint(
painter: RadarPulsePainter(),
),
)
3. Efficient Image Decoding, Caching & Memory Profiling
Displaying uncompressed 4K images inside small 50x50 avatars is a common cause of Out-Of-Memory (OOM) crashes. Always specify cacheWidth and cacheHeight in ResizeImage to decode only the exact required pixels into memory:
Image.network(
imageUrl,
cacheWidth: 150,
cacheHeight: 150,
)
4. Heavy Background Compute with Dart Isolates
Parsing huge JSON payloads (10,000+ items) or performing cryptographic image encryption on the main UI isolate causes noticeable frame stutter. Offload heavy computational work using Isolate.run() or the compute() helper:
final parsedData = await Isolate.run(() => parseLargeJson(rawPayload));