"My App Looked Like a Strobe Light."
It was a simple app fetching a list from an API. But whenever I clicked the 'Like' heart button in the list, the entire screen flashed (Flicker) subtly. Worse, if the list had images, they turned white and reloaded.
"I only toggled a tiny heart icon. Why is the entire Universe (Screen) redrawing?"
This was my mistake of misunderstanding Flutter's Rebuild Mechanism.
The Principle: The Culprit is FutureBuilder
The #1 most common mistake. Putting an async function directly into the future parameter of FutureBuilder.
class MyPage extends StatefulWidget {
@override
Widget build(BuildContext context) {
return FutureBuilder(
// 😱 getImages() runs EVERY time build() runs!
future: getImages(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator(); // Loading Spinner
}
return ListView(...);
},
);
}
}
Think about it.
- User clicks 'Like' ->
setState()called. setStatemeans "Redraw the screen (Rebuild)".build()executes again.getImages()is called again.FutureBuildergets a NEW Future, so it resets state towaiting.- Loading Spinner shows up for a split second, then list appears.
- Result: Screen flickers.
Solution 1: Isolate the Future
Create the Future object ONCE in initState.
class _MyPageState extends State<MyPage> {
// 1. Variable to store Future
late Future<List<Image>> _imagesFuture;
@override
void initState() {
super.initState();
// 2. Call API ONLY ONCE here
_imagesFuture = getImages();
}
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: _imagesFuture, // 3. Use stored variable
builder: ...,
);
}
}
Now, even if you call setState 100 times causing 100 build() runs, _imagesFuture remains the same completed object.
FutureBuilder won't go back to loading state. It shows data immediately. Flickering gone.
Solution 2: Magic of const Constructor
If it flickers without data loading involved, it might be unnecessary widget rendering.
The Alpha and Omega of Flutter optimization is const.
// ❌ Bad
Column(
children: [
Text("Static Title"), // Created new every time
MyDynamicWidget(),
],
)
// ✅ Good
Column(
children: [
const Text("Static Title"), // Compile-time constant (Reused)
MyDynamicWidget(),
],
)
Using const is like signing a contract with Flutter Engine: "This widget NEVER changes even if parent rebuilds. Just reuse it."
It drastically reduces the scope of Rebuilds.
Deep Dive: Tab Switch Flickering (KeepAlive)
When using PageView or TabBarView, switching tabs and coming back often resets scroll position or reloads data.
This is because widgets are Disposed when they leave the screen.
To prevent this, use AutomaticKeepAliveClientMixin.
class MyTab extends StatefulWidget { ... }
// 1. Add Mixin
class _MyTabState extends State<MyTab> with AutomaticKeepAliveClientMixin {
// 2. Return true
@override
bool get wantKeepAlive => true;
@override
Widget build(BuildContext context) {
super.build(context); // 3. MUST call super.build!
return ListView(...);
}
}
Now this tab stays alive in memory even when hidden. Scroll position preserves perfectly.
Tip: RepaintBoundary
If you have a complex drawing or animation inside a list, scrolling might lag.
Wrap that heavy widget in RepaintBoundary.
RepaintBoundary(
child: ComplexGraphWidget(),
)
This tells Flutter: "I will paint myself independently. Don't repaint me just because neighbor widgets changed." It saves GPU cycles.
7. Deep Dive: Debugging Tools (Performance Overlay)
"Where exactly is the rebuild happening?" Don't guess. See it.
- VS Code -> Command Palette (Cmd+Shift+P)
Flutter: Toggle Performance Overlay- Green graphs appear. Red spikes mean frame drops (Lag).
Even better: "Highlight Repaints" in DevTools. It draws rainbow borders around widgets that are repainting. If it flashes when you do nothing? Optimization Target 100%.
8. Case Study: Laggy Chat List
I built a chat room with thousands of messages. Fling scrolling caused stuttering.
The Cause
- Image Resizing: Resizing 4K user uploads to thumbnails on the UI thread.
- IntrinsicHeight: Used
IntrinsicHeightfor speech bubbles. (This makes layout calculation O(N^2)).
The Fix
- Server-Side Resizing: Never let the mobile resize. Server sends thumbnails.
- Cache Extent: Increased
ListView.cacheExtentto pre-render items off-screen. - const Bubbles: Extracted bubbles into
constwidgets.
Result: Solid 60fps. Optimization is mostly about "Stop doing unnecessary math."
9. Tip: JSON Parsing Blocking UI (Isolate)
Sometimes it's not the widget. It's User.fromJson blocking the UI thread.
If you parse a huge JSON (e.g., 10MB), the animation will freeze.
Use compute to move parsing to a background thread (Isolate).
// UI thread stays free!
final events = await compute(parseEvents, jsonString);
If animation janks, look for heavy computations.
Summary
"Flickering" is the biggest factor that makes an app feel "Cheap".
- Don't put
Futureinbuild. Move toinitState. - Habitualize
const. Turn on 'Prefer const' lint in VS Code. - Use
AutomaticKeepAliveClientMixinfor Tabs. - Use
CachedNetworkImagefor images. (DefaultImage.networkhas weak caching).
10. Deep Dive: The Double-Edged Sword of RepaintBoundary
"Should I wrap EVERYTHING in RepaintBoundary?" NO. It uses More Memory (VRAM). It creates a separate texture/layer for that widget.
When to use:
- Complex widgets taking > 20% of screen.
CustomPaint, Video, Maps.- Complex List Items.
When NOT to use:
- Simple
TextorIcon. - Static screens that don't animate.
11. Case Study: Saving Frames with const
I had a Dashboard updating 60 times/sec via Websocket. The UI thread was choking.
Before:
return Column(
children: [
HeaderWidget(), // ❌ Re-created 60 times/sec
GraphWidget(data: streamData),
FooterWidget(), // ❌ Re-created 60 times/sec
],
);
After:
return Column(
children: [
const HeaderWidget(), // ✅ Reused (Skipped Rebuild)
GraphWidget(data: streamData),
const FooterWidget(), // ✅ Reused (Skipped Rebuild)
],
);
Build time dropped from 16ms to 2ms.
Flutter Element Tree sees const and says "I know this guy, he hasn't changed," and skips the diffing algorithm entirely.
12. Performance: Opacity vs FadeTransition
Using Opacity widget for transparency effects?
It's a performance killer.
It forces a buffer redraw and compositing step.
For animations, FadeTransition is GPU-accelerated and much cheaper.
For static transparency, try Color.withOpacity or Container(color: ...) first.
Use Opacity only as a last resort.