Flutter apps are fast by default, but "fast enough" isn't the same as "optimized." After shipping 150+ production apps, we've developed a systematic approach to performance optimization that ensures every app we deliver runs at a buttery smooth 60fps (or 120fps on supported devices). This guide shares our battle-tested techniques.
Understanding the Frame Budget
At 60fps, you have approximately 16.67 milliseconds to build and render each frame. At 120fps, that drops to 8.33ms. Every millisecond matters. The two main threads to worry about are:
- UI Thread: Builds the widget tree, runs Dart code, handles layout
- Raster Thread: Converts the layer tree into GPU instructions, handles painting
If either thread exceeds the frame budget, you get jank — that visible stutter users hate.
1. Minimize Widget Rebuilds
The #1 performance issue we see in client codebases is unnecessary widget rebuilds. When
setState() is called, the entire widget subtree rebuilds. Here's how to minimize the blast
radius:
Use const Constructors
// BAD - rebuilds every frame
Widget build(BuildContext context) {
return Column(
children: [
Text('Static Header'),
Counter(count: count),
],
);
}
// GOOD - Header widget is cached
Widget build(BuildContext context) {
return Column(
children: [
const Text('Static Header'),
Counter(count: count),
],
);
}
The const keyword tells Flutter this widget never changes, so it skips rebuilding it entirely.
In large widget trees, this single optimization can eliminate thousands of unnecessary rebuilds per second.
Push State Down
Instead of calling setState() at the top of a large widget tree, extract the changing part into
its own widget with its own state. This limits rebuilds to only the widgets that actually changed.
2. ListView Optimization
Lists are where most performance problems surface. Here are the rules we follow:
- Always use
ListView.builderinstead ofListViewwith children — it lazily builds items only when they're visible - Specify
itemExtentwhen items have fixed height — it eliminates layout calculations - Use
constlist items when possible - Implement
addAutomaticKeepAlives: falseto dispose off-screen items
ListView.builder(
itemCount: items.length,
itemExtent: 72.0, // Fixed height = huge performance win
addAutomaticKeepAlives: false,
itemBuilder: (context, index) {
return ProductListTile(product: items[index]);
},
)
3. Image Optimization
Images are often the biggest performance bottleneck. Our approach:
- Always specify
cacheWidthandcacheHeightto decode images at display size, not original size - Use
cached_network_imagefor network images (caches to disk) - Implement progressive loading with low-res thumbnails as placeholders
- Convert images to WebP format server-side (30-40% smaller than JPEG)
Image.network(
imageUrl,
cacheWidth: 300, // Decode at 300px, not original 3000px
cacheHeight: 200,
fit: BoxFit.cover,
)
⚡ Quick Win
Adding cacheWidth and cacheHeight to just 10 images in a list view can reduce
memory usage by 80% or more.
4. Animation Performance
Animations should run on the raster thread when possible. Tips:
- Use
AnimatedBuilderinstead of rebuilding entire widgets - Prefer
TransformandOpacitywidgets for animations — they're composited on the GPU - Avoid
ClipRRectduring animations — clipping is expensive. UseContainerwithborderRadiusinstead when possible - Use
RepaintBoundaryto isolate frequently-animating widgets
5. Build Mode vs Profile Mode
Never measure performance in debug mode. Debug mode includes:
- Assertions and type checks
- Observatory debugger overhead
- Unoptimized compilation
Always profile with: flutter run --profile
This gives you near-release performance with DevTools access for measurement.
6. Shader Compilation Jank
The first time Flutter renders a specific visual effect, it compiles the shader for it. This can cause a one-time stutter. The solution: warm up shaders during app startup.
// In main.dart
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Warm up shaders
await ShaderWarmUp().warmUpShaderCache();
runApp(MyApp());
}
With Flutter's Impeller rendering engine (default on iOS, coming to Android), shader compilation jank is largely eliminated. But it's still worth knowing if you're targeting older Flutter versions.
7. Memory Management
- Dispose controllers in
dispose()— TextEditingController, AnimationController, StreamController - Cancel subscriptions — HTTP requests, streams, timers
- Use weak references for caches that should be cleared under memory pressure
- Monitor with DevTools — watch for memory that grows but never shrinks
Our Performance Checklist
Before every release, we run through this checklist:
- Profile mode testing on lowest-spec target device
- DevTools performance overlay shows no red bars
- No frame exceeds 16ms (or 8ms for 120Hz apps)
- Memory stays stable during extended use (no leaks)
- App size is optimized (tree shaking, deferred components)
- Network calls are batched and cached appropriately
- Images are resolution-appropriate and cached
Tools We Use
- Flutter DevTools: Performance overlay, timeline, memory view
- Performance overlay:
MaterialApp(showPerformanceOverlay: true) - Dart DevTools CPU Profiler: Find expensive function calls
- Custom Stopwatch: For measuring specific operations in code
Performance isn't an afterthought — it's a feature. Users may not notice a 60fps app, but they absolutely notice a 30fps one. Invest the time upfront, and your users (and app store ratings) will thank you.
🔍 Free Performance Audit
Struggling with app performance? We offer free 30-minute performance audits for Flutter apps. Book yours today.