Flutter: Otimização de Desempenho
Desempenho é a linha base da experiência do usuário — 60fps é uma promessa, 59fps é uma falha.
📋 Pré-requisitos: Você deve ter dominado o seguinte primeiro
- Aula 10: Lista e Rolagem
- Aula 12: Gerenciamento de Estado — Riverpod
1. O Que Você Vai Aprender
- Flutter DevTools: Performance Overlay / Widget Rebuild Tracker / CPU Profiler / Memory Chart
- Otimização de renderização: construtores const / RepaintBoundary / carregamento preguiçoso com ListView.builder / evitando rebuilds grandes de Widget
- Otimização de memória: estratégias de cache de imagem / AutomaticKeepAliveClientMixin / liberação de recursos em Dispose
- Otimização de rede: paginação de dados / CDN de imagens / cached_network_image / pré-carregamento
- ShopApp: otimização de rolagem a 60fps para lista de milhões de produtos
2. Uma História Real de Engasgos
(1) O Problema: Rolagem da Lista de Produtos a 30fps
A lista de produtos do ShopApp do Bob rola suavemente com 1000 itens, mas cai para 30fps com 10000 itens. O DevTools revela: cada card de produto dispara um rebuild durante a rolagem porque o setState do Widget pai causa o rebuild de toda a subárvore. Pior, as imagens não têm cache — toda vez que um item entra no viewport, ele é baixado novamente, e o uso de memória dispara para 500MB.
(2) A Solução: Otimização Direcionada
Otimização de desempenho não é "otimizar tudo" — é encontrar gargalos e atacá-los precisamente. DevTools aponta o problema → RepaintBoundary isola → const reduz rebuilds → cache de imagens reduz chamadas de rede.
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
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
(3) Resultado: Nível Milhão a 60fps + Memória Reduzida pela Metade
Após otimização direcionada, a lista de milhões de produtos do Bob mantém estáveis 60fps, a memória cai de 500MB para 200MB, e a taxa de acerto do cache de imagens atinge 95%.
3. Visão Geral da Otimização de Desempenho
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]
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
4. Análise de Desempenho com DevTools
(1) DevTools Principais
| Ferramenta | Propósito | Como Usar |
|---|---|---|
| Performance Overlay | FPS em tempo real + tempo da thread GPU | debugShowPerformanceOverlay: true |
| Widget Rebuild Tracker | Rastrear quais Widgets são rebuildados frequentemente | DevTools → Rebuild Tracker |
| CPU Profiler | Analisar hotspots de CPU | DevTools → CPU Profiler |
| Memory Chart | Alocação de memória & GC | DevTools → Memory |
| Network Inspector | Waterfall de requisições de rede | DevTools → Network |
| Flutter Inspector | Visualização da árvore de Widgets | DevTools → Inspector |
▶ Exemplo
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
: Habilitando análise de desempenho
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
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
| Modo de Execução | Otimização | Propósito |
|---|---|---|
| Debug | Sem otimização | Desenvolvimento & depuração |
| Profile | Quase-produção | Análise de desempenho |
| Release | Totalmente otimizado | Release de produção |
5. Otimização de Renderização
(1) Construtores const
| Comparação | Não-const | const |
|---|---|---|
| Comportamento de rebuild | Cria nova instância cada vez | Reutiliza a mesma instância |
| canUpdate | Pode não passar | Sempre passa |
| Impacto no desempenho | Sobrecarga de rebuilds repetidos | Zero sobrecarga de rebuild |
▶ Exemplo
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
: Card de produto otimizado com const
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
);
}
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
▶ Exemplo
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
: RepaintBoundary para isolar 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]),
),
)
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
(2) Evitando Rebuilds Grandes de Widget
| Problema | Solução |
|---|---|
| Método build grande rebuilda inteiro | Dividir em múltiplos Widgets menores |
| setState rebuilda toda a subárvore | Rebuildar apenas a parte afetada |
| Mudança no Widget pai afeta subárvore | Usar componentes filhos const para bloquear propagação |
▶ Exemplo
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
: Dividindo um Widget grande
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('+')),
]);
}
}
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
6. Otimização de Memória
▶ Exemplo
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
: 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]),
);
}
}
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
▶ Exemplo
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
: Liberação de recursos em Dispose
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();
}
}
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
7. Otimização de Rede
▶ Exemplo
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
: cached_network_image para cache de imagens
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
)
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
▶ Exemplo
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
: API Paginada + pré-carregamento
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]);
},
);
}
}
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
8. Exemplo Completo: Lista de Produtos de Alto Desempenho do ShopApp
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),
),
],
),
],
),
),
),
],
),
);
}
}
> Saída: Execute localmente com o Flutter SDK (Flutter 3.x / Dart 3.x). O servidor Piston não possui o Flutter instalado; use `flutter run` na sua máquina local para comparação. A UI/estado real pode variar ligeiramente entre plataformas.
❓ Perguntas Frequentes
P: Devo usar modo debug ou profile para análise de desempenho? R: Você deve usar modo profile. O modo debug inclui muitas asserções de verificação que tornam as leituras de FPS imprecisas.
P: Devo adicionar const onde for possível? R: Sim. Construtores const são otimizações de custo zero — você pode adicioná-los sempre que todos os parâmetros do construtor forem constantes em tempo de compilação.
P: Há desvantagem em adicionar muitos RepaintBoundaries? R: Sim. Cada RepaintBoundary cria uma camada de pintura independente, aumentando o uso de memória. Adicione apenas em pontos de isolamento onde a repintura é frequente.
P: Por que itemExtent melhora o desempenho? R: Com alturas fixas de itens, o ListView não precisa medir cada filho — pode calcular offsets diretamente, tornando a rolagem mais suave.
P: Qual a diferença entre CachedNetworkImage e Image.network? R: Image.network não tem cache e baixa novamente toda vez; CachedNetworkImage fornece automaticamente cache em disco + memória, e suporta widgets de placeholder e erro.
P: Como detecto vazamentos de memória? R: Use o painel Memory do DevTools para ver tendências de memória. Se a memória continua crescendo e nunca volta, há provavelmente um vazamento. Causas comuns: Controllers não descartados, Streams não cancelados.
📖 Resumo
- DevTools aponta gargalos de desempenho: análise FPS / Rebuild / CPU / Memory em quatro dimensões
- Construtores const são otimizações de custo zero — adicione onde for possível
- RepaintBoundary isola repaints, impedindo que pequenas mudanças disparem repinturas em grande área
- Divida Widgets grandes para que setState afete apenas a subárvore mínima
- CachedNetworkImage faz cache de imagens; AutomaticKeepAlive preserva estado de Tab
📝 Exercícios
- Básico (dificuldade ⭐): Use o Widget Rebuild Tracker no DevTools para encontrar Widgets frequentemente rebuildados, e otimize-os com const.
- Intermediário (dificuldade ⭐⭐): Adicione RepaintBoundary + itemExtent a uma lista de produtos, e compare o FPS antes e depois da otimização.
- Desafio (dificuldade ⭐⭐⭐): Implemente uma lista de produtos completa de alto desempenho: const Widget + RepaintBoundary + CachedNetworkImage(memCacheWidth) + pré-carregamento paginado + AutomaticKeepAliveClientMixin, alcançando 60fps estáveis com 10000 itens em modo profile.