| name | flutter-performance-docs |
| description | [Flutter] Flutter performance best practices and optimization guide. Build cost, rendering, lists, animations, and anti-patterns. (project) |
Flutter Performance Best Practices
The 16ms Rule
| Display | Frame Budget | Build | Render |
|---|---|---|---|
| 60Hz | 16ms | ~8ms | ~8ms |
| 120Hz | 8ms | ~4ms | ~4ms |
Always profile in profile mode, not debug mode.
1. Control build() Cost
Split Large Widgets
// BAD: Monolithic widget
class MyPage extends StatelessWidget {
Widget build(context) => Column(children: [header, content, footer]);
}
// GOOD: Split into smaller widgets
class MyPage extends StatelessWidget {
Widget build(context) => Column(children: [
HeaderWidget(),
ContentWidget(),
FooterWidget(),
]);
}
Localize setState()
// BAD: setState high in tree
class Parent extends StatefulWidget {
void update() => setState(() {}); // Rebuilds entire subtree
}
// GOOD: setState only where needed
class Child extends StatefulWidget {
void update() => setState(() {}); // Only rebuilds this widget
}
Use const Constructors
// GOOD: Flutter skips rebuild for const widgets
const Text('Hello');
const SizedBox(height: 16);
const MyCustomWidget();
Prefer StatelessWidget Over Functions
// BAD: Function returns widget
Widget buildHeader() => Container(...);
// GOOD: StatelessWidget (enables const, better rebuild tracking)
class Header extends StatelessWidget {
const Header();
Widget build(context) => Container(...);
}
2. Lists & Grids
Use Lazy Builders
// BAD: Builds all items at once
ListView(children: items.map((i) => ItemWidget(i)).toList())
// GOOD: Only builds visible items
ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) => ItemWidget(items[index]),
)
Avoid Intrinsic Passes
Intrinsic passes poll all cells for sizing - expensive for large grids.
// BAD: Causes intrinsic pass
IntrinsicHeight(child: Row(children: [...]))
// GOOD: Fixed sizes
SizedBox(height: 100, child: Row(children: [...]))
Debug: Enable "Track layouts" in DevTools to see intrinsic timeline events.
3. Minimize saveLayer()
saveLayer() allocates offscreen buffer - expensive!
Widgets That Trigger saveLayer()
ShaderMaskColorFilterChip(ifdisabledColorAlpha != 0xff)Text(withoverflowShader)
Debug
Enable PerformanceOverlayLayer.checkerboardOffscreenLayers in DevTools.
4. Opacity & Clipping
Opacity
// BAD: Wraps widget in Opacity
Opacity(opacity: 0.5, child: Image(...))
// GOOD: Apply directly to image
Image(..., color: Colors.white.withOpacity(0.5), colorBlendMode: BlendMode.modulate)
// GOOD: For text, use semitransparent color
Text('Hello', style: TextStyle(color: Colors.black54))
// GOOD: For animations
AnimatedOpacity(opacity: _visible ? 1.0 : 0.0, child: ...)
FadeInImage(placeholder: ..., image: ...)
Clipping
// BAD: Explicit clipping
ClipRRect(borderRadius: BorderRadius.circular(8), child: Container(...))
// GOOD: Use decoration borderRadius
Container(
decoration: BoxDecoration(borderRadius: BorderRadius.circular(8)),
child: ...
)
Default is Clip.none - enable clipping only when needed.
5. Animations
TransitionBuilder Pattern
// BAD: Rebuilds everything
AnimatedBuilder(
animation: _controller,
builder: (context, child) => Transform.rotate(
angle: _controller.value,
child: ExpensiveWidget(), // Rebuilt every frame!
),
)
// GOOD: Child is not rebuilt
AnimatedBuilder(
animation: _controller,
child: ExpensiveWidget(), // Built once
builder: (context, child) => Transform.rotate(
angle: _controller.value,
child: child, // Reused
),
)
Pre-clip Images for Animation
// BAD: Clips during animation
AnimatedBuilder(
builder: (_, __) => ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image(...),
),
)
// GOOD: Pre-clipped image
final clippedImage = ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image(...),
);
AnimatedBuilder(
child: clippedImage,
builder: (_, child) => Transform.scale(scale: _scale, child: child),
)
6. String Concatenation
// BAD: Creates intermediate strings
String result = '';
for (var item in items) {
result += item.toString(); // O(n²)
}
// GOOD: Single concatenation
final buffer = StringBuffer();
for (var item in items) {
buffer.write(item.toString());
}
final result = buffer.toString(); // O(n)
Anti-Patterns Summary
| Anti-Pattern | Solution |
|---|---|
Opacity widget in animations |
AnimatedOpacity, FadeInImage |
ListView(children: [...]) |
ListView.builder() |
ClipRRect in animations |
Pre-clip before animating |
Override operator == on Widget |
Only for leaf widgets with efficient comparison |
| Large monolithic widgets | Split into smaller widgets |
setState() high in tree |
Localize to affected subtree |
IntrinsicHeight/Width |
Fixed sizes or custom RenderObject |
Debugging Tools
| Tool | Purpose |
|---|---|
| DevTools Performance View | Timeline, frame analysis |
| DevTools Inspector | Track widget rebuilds |
| "Track layouts" option | Find intrinsic passes |
checkerboardOffscreenLayers |
Find saveLayer calls |
| Profile mode build | Accurate performance measurement |
Mobile: Use Impeller
Impeller is Flutter's default graphics renderer. Eliminates shader compilation jank.
# Verify Impeller is enabled (default on iOS/Android)
flutter run --enable-impeller