Performance Optimization
Writing fast, smooth Flutter apps that deliver 60fps experiences on any device.
Generally, Flutter applications are performant by default , but you only need to avoid common pitfalls to get excellent performance. These best practice recommendations will help you write the most performant Flutter app possible.
🎯 The 16ms Rule
To achieve 60 frames per second (fps), each frame must be built and rendered in 16ms or less . Since Flutter has separate threads for building and rendering, you have 16ms for building and 16ms for rendering. For 120fps devices, aim for under 8ms total.
The
build()
method is called frequently — every time an ancestor widget rebuilds.
Here's how to keep it efficient:
// ❌ Bad: setState at top level rebuilds everything
class
BadExample
extends
StatefulWidget {
@override
_BadExampleState
createState
() => _BadExampleState();
}
class
_BadExampleState
extends
State<BadExample> {
int
_counter =
0
;
void
_increment
() {
setState
(() {
// Rebuilds the ENTIRE widget tree!
_counter++;
});
}
@override
Widget
build
(BuildContext context) {
return
Column(
children: [
Text(
'Counter: $_counter'
),
Expanded(
child: ListView.builder(...),
// This rebuilds too!
),
],
);
}
}
// ✅ Good: Localize setState to only what changes
class
GoodExample
extends
StatefulWidget {
@override
_GoodExampleState
createState
() => _GoodExampleState();
}
class
_GoodExampleState
extends
State<GoodExample> {
int
_counter =
0
;
void
_increment
() {
setState
(() {
// Only rebuilds this part
_counter++;
});
}
@override
Widget
build
(BuildContext context) {
return
Column(
children: [
// Wrap changing part in its own widget
CounterDisplay(counter: _counter, onIncrement: _increment),
Expanded(
child: MyListView(),
// Won't rebuild when counter changes
),
],
);
}
}
// ❌ Bad: Non-const widget rebuilds every time
class
BadButton
extends
StatelessWidget {
final
String text;
BadButton
(
this
.text);
// Not const
@override
Widget
build
(BuildContext context) {
return
Text(text);
}
}
// ✅ Good: Const widget can be optimized
class
GoodButton
extends
StatelessWidget {
final
String text;
const
GoodButton
(
this
.text);
// Const constructor
@override
Widget
build
(BuildContext context) {
return
Text(text);
}
}
// Usage
const
GoodButton(
'Click Me'
);
// Flutter can short-circuit rebuilds
// ❌ Bad: Function returns a widget
Widget
_buildHeader
() {
return
Text(
'Header'
);
}
// ✅ Good: StatelessWidget
class
HeaderWidget
extends
StatelessWidget {
const
HeaderWidget
();
@override
Widget
build
(BuildContext context) {
return
Text(
'Header'
);
}
}
✅ Build Cost Tips
- Localize setState() – Only rebuild what needs to change
- Use const widgets – Enables Flutter to short-circuit rebuilds
- Use StatelessWidget – Prefer widgets over helper functions
- Split large widgets – Break into smaller, focused widgets
- Use AnimatedBuilder child parameter – Pre-build subtrees that don't change
1. Avoid saveLayer()
saveLayer()
allocates an offscreen buffer and can cause jank. Widgets that trigger it:
-
Opacity(when opacity is not 0 or 1) -
ShaderMask -
ColorFilter -
Chip(when disabledColorAlpha != 0xff) -
Text(with overflowShader)
// Each of these creates a saveLayer() call
Opacity(
opacity:
0.5
,
child: Container(color: Colors.blue),
)
// ✅ Good: Use semi-transparent color instead
Container(
color: Colors.blue.
withOpacity
(
0.5
),
)
// ✅ Good: Use FadeInImage for fading images
FadeInImage.assetNetwork(
placeholder:
'assets/placeholder.png'
,
image:
'https://example.com/image.jpg'
,
)
2. Minimize Clipping
Clipping is costly but doesn't use
saveLayer()
(unless using
Clip.antiAliasWithSaveLayer
). However, it's still expensive.
// ❌ Bad: ClipRRect for rounded corners
ClipRRect(
borderRadius: BorderRadius.
circular
(
12
),
child: Container(
width:
100
,
height:
100
,
color: Colors.blue,
),
)
// ✅ Good: Use borderRadius property
Container(
width:
100
,
height:
100
,
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.
circular
(
12
),
),
)
3. Use StringBuffer for String Building
String
buildString
(List<String> items) {
String result =
''
;
for
(
var
item
in
items) {
result +=
'$item, '
;
// Creates new String each time
}
return
result;
}
// ✅ Good: Use StringBuffer
String
buildString
(List<String> items) {
final
buffer = StringBuffer();
for
(
var
item
in
items) {
buffer.
write
(
'$item, '
);
}
return
buffer.
toString
();
// One concatenation only
}
Be lazy! When building large grids or lists, use the lazy builder methods with callbacks. This ensures that only the visible portion of the screen is built at startup time.
// ❌ Bad: ListView with all children built at once
ListView(
children: [
for
(
var
i
in
List.
generate
(
1000
, (i) => i))
ListTile(title: Text(
'Item $i'
)),
],
)
// ✅ Good: ListView.builder (lazy loading)
ListView.builder(
itemCount:
1000
,
itemBuilder: (context, index) {
return
ListTile(title: Text(
'Item $index'
));
},
)
// ✅ Good: GridView.builder
GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount:
3
,
),
itemCount:
1000
,
itemBuilder: (context, index) {
return
Container(
color: Colors.primaries[index % Colors.primaries.
length
],
);
},
)
💡 Lazy Loading Benefits
- Faster initial load – Only builds what's visible
- Less memory usage – Widgets are created and destroyed as needed
- Better performance – Fewer widgets to maintain
- Scalable – Can handle thousands of items
An intrinsic pass is a second layout pass that happens when widgets need to know the intrinsic size of their children (e.g., to make all cells the same size). This doubles the layout cost.
// IntrinsicHeight causes a second layout pass
Column(
children: [
IntrinsicHeight(
child: Row(
children: [
Expanded(child: Container()),
Expanded(child: Container()),
],
),
),
ListView.builder(...),
// This will be affected by intrinsic pass
],
)
✅ How to Avoid Intrinsic Passes
-
Set fixed sizes
– Use
SizedBoxwith fixed dimensions - Use constraints – Let parent widgets provide constraints
-
Use
ConstrainedBox– Set min/max constraints -
Use
LayoutBuilder– Get parent constraints dynamically
Animations can be expensive. Here's how to keep them smooth:
class
OptimizedAnimation
extends
StatefulWidget {
@override
_OptimizedAnimationState
createState
() => _OptimizedAnimationState();
}
class
_OptimizedAnimationState
extends
State<OptimizedAnimation>
with
SingleTickerProviderStateMixin {
late
AnimationController _controller;
@override
void
initState
() {
super
.
initState
();
_controller = AnimationController(
duration: Duration(seconds:
2
),
vsync:
this
,
)..
repeat
();
}
@override
void
dispose
() {
_controller.
dispose
();
super
.
dispose
();
}
@override
Widget
build
(BuildContext context) {
// ❌ Bad: Builder rebuilds everything on each tick
return
AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return
Column(
children: [
Text(
'Header'
),
// Rebuilds every tick
Transform.scale(
scale: _controller.value,
child: FlutterLogo(size:
100
),
),
],
);
},
);
// ✅ Good: Pre-build static parts
final
header = Text(
'Header'
);
// Built once
return
AnimatedBuilder(
animation: _controller,
child: header,
// Pass as child, won't rebuild
builder: (context, child) {
return
Column(
children: [
child!,
// Static part
Transform.scale(
scale: _controller.value,
child: FlutterLogo(size:
100
),
),
],
);
},
);
}
}
💡 Animation Optimization Tips
- Use AnimatedBuilder child – Pre-build static subtrees
-
Avoid Opacity in animations
– Use
FadeInImageorAnimatedOpacity - Avoid clipping in animations – Pre-clip images before animating
-
Use
RepaintBoundary– Isolate expensive repaints
Follow these steps to profile and optimize your Flutter app:
flutter run --profile
for accurate performance metrics
- Using Opacity in animations – Each frame triggers saveLayer()
- Large build() methods – Rebuild too much when setState() is called
- Non-lazy lists – Building all items at once for large lists
- Intrinsic widgets in lists – Causes expensive second layout pass
- Overriding operator== on widgets – Causes O(N²) behavior
- Helper functions for widgets – Can't be optimized like const widgets
- Not using const constructors – Misses short-circuit optimization
✅ Quick Wins
-
Add
constto constructors where possible -
Use
ListView.builderinstead ofListView(children: []) -
Use
AnimatedBuilderwith child parameter for static subtrees -
Replace
OpacitywithwithOpacity()on colors -
Replace
ClipRRectwithborderRadiusin decorations -
Use
StringBufferinstead of+for string concatenation
📚 Learning Resources
- Flutter Performance Best Practices – Official documentation
- DevTools Documentation – Performance profiling tools
- Performance View – Using DevTools for profiling
- Rendering Performance – Deep dive into rendering
🔗 Related Lessons
- Animations – Performance considerations for animations
- Slivers – Performance in custom scrolling
- DevTools – Using tools to measure performance
🎯 Key Takeaway
Performance optimization
is about being intentional with how you build your
Flutter app. Focus on
controlling build cost
— localize
setState()
,
use
const
constructors, and prefer widgets over functions.
Minimize expensive
operations
like
saveLayer()
, opacity, and clipping.
Be lazy
with lists and grids using builder methods. Use
DevTools
to profile and identify
bottlenecks. Remember, Flutter apps are performant by default — avoid common pitfalls and you'll
deliver smooth 60fps experiences.