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.
- 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(...); }
- 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.
- Minimize saveLayer()
saveLayer() allocates offscreen buffer - expensive!
Widgets That Trigger saveLayer()
-
ShaderMask
-
ColorFilter
-
Chip (if disabledColorAlpha != 0xff )
-
Text (with overflowShader )
Debug
Enable PerformanceOverlayLayer.checkerboardOffscreenLayers in DevTools.
- 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.
- 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), )
- 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
Official Docs
-
Performance best practices
-
Rendering performance
-
DevTools Performance View