Flutter: Performance Optimization
Performance is the baseline of user experience — 60fps is a promise, 59fps is a failure.
📋 Prerequisites: You should have mastered the following first
- Lesson 10: List and Scrolling
- Lesson 12: State Management — Riverpod
1. What You Will Learn
- Flutter DevTools: Performance Overlay / Widget Rebuild Tracker / CPU Profiler / Memory Chart
- Render optimization: const constructors / RepaintBoundary / ListView.builder lazy loading / avoiding large Widget rebuilds
- Memory optimization: image caching strategies / AutomaticKeepAliveClientMixin / Dispose resource release
- Network optimization: data pagination / image CDN / cached_network_image / preloading
- ShopApp: 60fps scrolling optimization for a million-product list
2. A Real Story of Jank
(1) The Pain: Product List Scrolling at 30fps
Bob's ShopApp product list scrolls smoothly at 1000 items, but drops to 30fps at 10000 items. DevTools reveals: every product card triggers a rebuild during scrolling because the parent Widget's setState causes the entire subtree to rebuild. Worse, images have no caching — every time an item scrolls into the viewport, it re-downloads, and memory usage spikes to 500MB.
(2) The Solution: Targeted Optimization
Performance optimization is not "optimize everything" — it's finding bottlenecks and targeting them precisely. DevTools pinpoints the problem → RepaintBoundary isolates → const reduces rebuilds → image caching reduces network calls.
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
// Dependency: cached_network_image: ^3.0.0 (add to dependencies)
// Product comes from the ShopApp project, full definition in Lesson 26
// Key optimizations
const ProductCard(...); // 1. const avoids rebuild
RepaintBoundary(child: Image.network(...)); // 2. Isolate repaint
CachedNetworkImage(imageUrl: ...); // 3. Cache images
AutomaticKeepAliveClientMixin; // 4. Keep tab state
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; please use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly by platform.
(3) The Result: Million-level 60fps + Memory Halved
After targeted optimization, Bob's million-product list maintains a stable 60fps, memory drops from 500MB to 200MB, and image cache hit rate reaches 95%.
3. Performance Optimization Overview
graph TD
PO[Performance Optimization] --> DEV[DevTools]
PO --> RO[Render Optimization]
PO --> MO[Memory Optimization]
PO --> NO[Network Optimization]
RO --> |const| CW[const Widgets]
RO --> |RepaintBoundary| RB2[Isolate Repaint]
MO --> |cache| IMG[Image Cache]
NO --> |pagination| PG[Pagination API]
DEV --> |Overlay| FPS[FPS Monitor]
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; please use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly by platform.
4. DevTools Performance Analysis
(1) Core DevTools
| Tool | Purpose | How to Use |
|---|---|---|
| Performance Overlay | Real-time FPS + GPU thread time | debugShowPerformanceOverlay: true |
| Widget Rebuild Tracker | Track which Widgets rebuild frequently | DevTools → Rebuild Tracker |
| CPU Profiler | Analyze CPU hotspots | DevTools → CPU Profiler |
| Memory Chart | Memory allocation & GC | DevTools → Memory |
| Network Inspector | Network request waterfall | DevTools → Network |
| Flutter Inspector | Widget tree visualization | DevTools → Inspector |
▶ Example
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; please use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly by platform.
: Enabling performance analysis
import 'package:flutter/material.dart';
// Development mode: enable performance overlay
MaterialApp(
debugShowPerformanceOverlay: true,
showSemanticsDebugger: false,
// ...
)
// Profile mode: accurate performance measurement
// Run: flutter run --profile
// Then open DevTools: flutter pub global run devtools
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; please use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly by platform.
| Run Mode | Optimization | Purpose |
|---|---|---|
| Debug | No optimization | Development & debugging |
| Profile | Near-production | Performance analysis |
| Release | Fully optimized | Production release |
5. Render Optimization
(1) const Constructors
| Comparison | Non-const | const |
|---|---|---|
| Rebuild behavior | Creates a new instance each time | Reuses the same instance |
| canUpdate | May not pass | Always passes |
| Performance impact | Overhead from repeated rebuilds | Zero rebuild overhead |
▶ Example
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; please use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly by platform.
: Const-optimized product card
import 'package:flutter/material.dart';
// BAD: not const, rebuilds every time
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.all(8.0), // New instance each build
child: Text('Price: \$${product.price}'),
);
}
// GOOD: const where possible
Widget build(BuildContext context) {
return const Padding( // const padding
padding: EdgeInsets.all(8.0), // Compile-time constant
child: Text('Fixed text'), // const if content is constant
);
}
// For dynamic content, const the wrapper
Widget build(BuildContext context) {
return const Padding(
padding: EdgeInsets.all(8.0),
child: _DynamicPrice(product: product), // Can't const dynamic
);
}
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; please use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly by platform.
▶ Example
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; please use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly by platform.
: RepaintBoundary to isolate repaints
import 'package:flutter/material.dart';
// Product comes from the ShopApp project, full definition in Lesson 26
// Without RepaintBoundary: entire list repaints when one item changes
ListView.builder(
itemCount: 1000,
itemBuilder: (_, i) => ProductCard(product: products[i]),
)
// With RepaintBoundary: only changed card repaints
ListView.builder(
itemCount: 1000,
itemBuilder: (_, i) => RepaintBoundary(
key: ValueKey(products[i].id),
child: ProductCard(product: products[i]),
),
)
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; please use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly by platform.
(2) Avoiding Large Widget Rebuilds
| Problem | Solution |
|---|---|
| Large build method rebuilds entirely | Split into multiple smaller Widgets |
| setState rebuilds the entire subtree | Only rebuild the affected part |
| Parent Widget changes affect subtree | Use const child components to block propagation |
▶ Example
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; please use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly by platform.
: Splitting a large Widget
import 'package:flutter/material.dart';
// BAD: entire page rebuilds on counter change
class BadPage extends StatefulWidget {
@override
State<BadPage> createState() => _BadPageState();
}
class _BadPageState extends State<BadPage> {
int _count = 0;
@override
Widget build(BuildContext context) {
return Column(children: [
const HeavyHeader(), // Rebuilds unnecessarily!
const ProductGrid(), // Rebuilds unnecessarily!
Text('Count: $_count'), // Only this needs rebuild
ElevatedButton(onPressed: () => setState(() => _count++), child: const Text('+')),
]);
}
}
// GOOD: only counter part rebuilds
class GoodPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(children: [
const HeavyHeader(), // Never rebuilds (const)
const ProductGrid(), // Never rebuilds (const)
const _CounterSection(), // Only this rebuilds
]);
}
}
// HeavyHeader / ProductGrid are placeholder example Widgets
class HeavyHeader extends StatelessWidget {
const HeavyHeader({super.key});
@override Widget build(BuildContext context) => const Text('Header');
}
class ProductGrid extends StatelessWidget {
const ProductGrid({super.key});
@override Widget build(BuildContext context) => const Text('Grid');
}
class _CounterSection extends StatefulWidget {
const _CounterSection();
@override
State<_CounterSection> createState() => _CounterSectionState();
}
class _CounterSectionState extends State<_CounterSection> {
int _count = 0;
@override
Widget build(BuildContext context) {
return Column(children: [
Text('Count: $_count'),
ElevatedButton(onPressed: () => setState(() => _count++), child: const Text('+')),
]);
}
}
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; please use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly by platform.
6. Memory Optimization
▶ Example
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; please use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly by platform.
: AutomaticKeepAliveClientMixin
import 'package:flutter/material.dart';
// Product / ProductTile come from the ShopApp project, full definitions in Lessons 26/27
// Keep tab content alive when switching tabs
class ProductListTab extends StatefulWidget {
@override
State<ProductListTab> createState() => _ProductListTabState();
}
class _ProductListTabState extends State<ProductListTab>
with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true; // Keep state when tab is hidden
@override
Widget build(BuildContext context) {
super.build(context); // Required for mixin
return ListView.builder(
itemCount: products.length,
itemBuilder: (_, i) => ProductTile(product: products[i]),
);
}
}
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; please use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly by platform.
▶ Example
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; please use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly by platform.
: Dispose resource release
import 'dart:async';
import 'package:flutter/material.dart';
// DetailPage / productStream come from the ShopApp project
class _DetailPageState extends State<DetailPage> with SingleTickerProviderStateMixin {
late AnimationController _controller;
late TextEditingController _textController;
StreamSubscription? _subscription;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this, duration: const Duration(seconds: 1));
_textController = TextEditingController();
_subscription = productStream.listen((_) => setState(() {}));
}
@override
void dispose() {
_controller.dispose(); // Release animation
_textController.dispose(); // Release text controller
_subscription?.cancel(); // Cancel stream
super.dispose();
}
}
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; please use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly by platform.
7. Network Optimization
▶ Example
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; please use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly by platform.
: cached_network_image for image caching
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
// Dependency: cached_network_image: ^3.0.0 (add to dependencies)
// product.imageUrl comes from ShopApp Product entity
CachedNetworkImage(
imageUrl: product.imageUrl,
placeholder: (context, url) => Container(
color: Colors.grey[200],
child: const Center(child: CircularProgressIndicator()),
),
errorWidget: (context, url, error) => const Icon(Icons.error),
fadeInDuration: const Duration(milliseconds: 300),
memCacheWidth: 400, // Resize in memory to save RAM
maxWidth: 800, // Disk cache max width
)
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; please use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly by platform.
▶ Example
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; please use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly by platform.
: Paginated API + preloading
import 'package:flutter/material.dart';
// Dependency: no extra dependencies (Dart/Flutter standard library sufficient)
// Product / ProductTile / LoadingIndicator come from the ShopApp project
// api.fetchProducts comes from the ShopApp network layer
class PaginatedProductList extends StatefulWidget {
@override
State<PaginatedProductList> createState() => _PaginatedProductListState();
}
class _PaginatedProductListState extends State<PaginatedProductList> {
final _controller = ScrollController();
List<Product> _products = [];
int _page = 1;
bool _hasMore = true;
@override
void initState() {
super.initState();
_loadPage();
_controller.addListener(() {
// Preload when 2 screens away from bottom
if (_controller.position.pixels >= _controller.position.maxScrollExtent * 0.8) {
_loadPage();
}
});
}
Future<void> _loadPage() async {
if (!_hasMore) return;
final newProducts = await api.fetchProducts(page: _page, limit: 20);
setState(() {
_products.addAll(newProducts);
_page++;
_hasMore = newProducts.length >= 20;
});
}
@override
Widget build(BuildContext context) {
return ListView.builder(
controller: _controller,
itemCount: _products.length + (_hasMore ? 1 : 0),
itemExtent: 72, // Fixed height for performance
itemBuilder: (_, i) {
if (i == _products.length) return const LoadingIndicator();
return ProductTile(product: _products[i]);
},
);
}
}
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; please use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly by platform.
8. Complete Example: ShopApp High-Performance Product List
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:cached_network_image/cached_network_image.dart';
// Dependencies: flutter_riverpod: ^2.0.0, cached_network_image: ^3.0.0
// Product / productProvider / cartProvider come from the ShopApp project, full definitions in Lessons 26/27
class OptimizedProductGrid extends ConsumerWidget {
const OptimizedProductGrid({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final productsAsync = ref.watch(productProvider);
return productsAsync.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => ErrorWidget(e),
data: (products) => GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
childAspectRatio: 0.7,
),
itemCount: products.length,
itemBuilder: (_, i) => RepaintBoundary(
key: ValueKey(products[i].id),
child: _OptimizedProductCard(product: products[i]),
),
),
);
}
}
class _OptimizedProductCard extends ConsumerWidget {
final Product product;
const _OptimizedProductCard({required this.product});
@override
Widget build(BuildContext context, WidgetRef ref) {
return Card(
clipBehavior: Clip.antiAlias,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
flex: 3,
child: RepaintBoundary( // Isolate image repaint
child: CachedNetworkImage(
imageUrl: product.imageUrl,
fit: BoxFit.cover,
memCacheWidth: 200, // Save memory
placeholder: (_, __) => Container(color: Colors.grey[200]),
errorWidget: (_, __, ___) => const Icon(Icons.image_not_supported),
),
),
),
Expanded(
flex: 2,
child: Padding(
padding: const EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(product.name, maxLines: 1, overflow: TextOverflow.ellipsis),
const Spacer(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('\$${product.price.toStringAsFixed(2)}',
style: const TextStyle(color: Colors.green)),
IconButton(
icon: const Icon(Icons.add_shopping_cart, size: 18),
onPressed: () => ref.read(cartProvider.notifier).addItem(product),
),
],
),
],
),
),
),
],
),
);
}
}
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; please use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly by platform.
❓ FAQ
📖 Summary
- DevTools pinpoints performance bottlenecks: FPS / Rebuild / CPU / Memory analysis across four dimensions
- const constructors are zero-cost optimizations — add them wherever possible
- RepaintBoundary isolates repaints, preventing small changes from triggering large-area repaints
- Split large Widgets so setState only affects the minimal subtree
- CachedNetworkImage caches images; AutomaticKeepAlive preserves Tab state
📝 Exercises
- Basic (Difficulty ⭐): Use the Widget Rebuild Tracker in DevTools to find frequently rebuilt Widgets, and optimize them with const.
- Intermediate (Difficulty ⭐⭐): Add RepaintBoundary + itemExtent to a product list, and compare FPS before and after optimization.
- Challenge (Difficulty ⭐⭐⭐): Implement a complete high-performance product list: const Widget + RepaintBoundary + CachedNetworkImage(memCacheWidth) + paginated preloading + AutomaticKeepAliveClientMixin, achieving stable 60fps with 10000 items in profile mode.