Boas práticas de performance no Flutter
Otimizando seu app para máxima eficiência e fluidez
O Flutter é um framework altamente otimizado, com uma engine em constante evolução. No entanto, mesmo com toda essa eficiência, a performance de um app ainda depende fortemente das decisões do desenvolvedor. Um uso inadequado do framework pode facilmente comprometer a experiência do usuário com travamentos, lentidão e consumo excessivo de recursos.
Neste artigo, reunimos uma série de boas práticas para evitar esses problemas e garantir que seu app Flutter seja rápido, fluido e eficiente.
Evite Operações Custosas no build()
O método build()
é executado na UI thread, ou seja, qualquer operação pesada ali pode causar janks (travamentos visíveis). Prefira mover essas operações para:
initState()
didChangeDependencies()
didUpdateWidget()
Se a operação for muito custosa, utilize Isolates para processá-la em uma thread separada.
Otimize consultas utilizando BuildContext
Utilização de InheritedWidget em excesso, sem tomar devidos cuidados, pode trazer baixa performance para o aplicativo. Quando utilizamos MediaQuery.of(context) ou Theme.of(context) (ou outros InheritedWidget's) sem fazer “cache” deles e a arvore de widgets for bem profunda, Flutter percorre a arvore para encontrar a instancia dessa classe e registra a dependência para ser rebuildada, enquanto a tela esta sendo construída. Isso, em escala, pode trazer janks para a aplicação.
A melhor forma de resolver isso é evitar fazer essa “busca profunda”, com frequencia é armazenando os valores dentro de variáveis, dentro do metodo build() e reutiliza-las.
❌ Exemplo não otimizado:
Widget build(BuildContext context) {
return Container(
color: Theme.of(context).colorScheme.background,
padding: EdgeInsets.all(MediaQuery.of(context).size.width * 0.05),
child: Column(
children: [
Text('Price', style: Theme.of(context).textTheme.titleMedium),
Text('\$${product.price}', style: Theme.of(context).textTheme.headlineMedium),
SizedBox(height: MediaQuery.of(context).size.height * 0.02),
// More widgets with repetitive lookups...
],
),
);
}
✅ Exemplo otimizado:
Widget build(BuildContext context) {
final theme = Theme.of(context);
final mediaQuery = MediaQuery.of(context);
final size = mediaQuery.size;
return Container(
color: theme.colorScheme.background,
padding: EdgeInsets.all(size.width * 0.05),
child: Column(
children: [
Text('Price', style: theme.textTheme.titleMedium),
Text('\$${product.price}', style: theme.textTheme.headlineMedium),
SizedBox(height: size.height * 0.02),
// More widgets using cached values...
],
),
);
}
Use const
e Key
Sempre que Possível
Utilize construtores
const
para evitar reconstruções desnecessárias.Use
Key
para ajudar o Flutter a identificar widgets que não precisam ser reconstruídos.Mantenha widgets estáticos fora de árvores que mudam com frequência.
Evite ClipRRect
para borderRadius
Prefira usar o borderRadius
diretamente em widgets como Container
. O ClipRRect
, especialmente com Clip.antiAliasWithSaveLayer
, é mais custoso pois utiliza SaveLayer
, criando buffers offscreen.
Cuidado com Widgets que Usam SaveLayer
Widgets como Opacity
, ShaderMask
e ColorFilter
utilizam SaveLayer
. Prefira alternativas como:
// Em vez de: Opacity(opacity: 0.5, child: MyWidget())
// Use: Container(color: Colors.red.withOpacity(0.5), child: MyWidget())
Identifique Uso Desnecessário de SaveLayer
Ative checkerboardOffscreenLayers
no MaterialApp
ou via DevTools para visualizar componentes que usam SaveLayer
.
Ordene Condições de if
por Custo
Evite avaliar cláusulas pesadas primeiro:
// Errado: if (heavy && medium && light) { ... }
// Correto: if (light && medium && heavy) { ... }
Você também pode usar late final
para atrasar a execução:
late final op1 = expensiveOp1();
late final op2 = expensiveOp2();
if (condition && op1 && op2) { ... }
Libere Objetos Nullable
Se você tiver objetos que podem ser null
e não são mais necessários, atribua null
explicitamente para permitir que o garbage collector os limpe.
Problemas com ListView
Dentro de ScrollView
Evite usar ListView
com shrinkWrap: true
e NeverScrollableScrollPhysics
dentro de SingleChildScrollView
. Isso força a renderização de todos os itens de uma vez.
❌ Exemplo de ListView com baixa performance:
SingleChildScrollView(
child: Column(
children: [
const Card(child: Text("Header card")),
ListView.builder(
physics: const NeverScrollableScrollPhysics(),
itemCount: 1000,
shrinkWrap: true,
itemBuilder: (context, index) {
print("building item #${index}");
return Card(child: Text(index.toString()));
},
),
const Card(child: Text("Footer card")),
],
),
)
✅ Solução: utilize Slivers:
CustomScrollView(
slivers: [
const SliverToBoxAdapter(child: Card(child: Text("Header card"))),
SliverList.builder(
itemBuilder: (context, index) {
print("building item #${index}");
return Card(child: Text(index.toString()));
},
),
const SliverToBoxAdapter(child: Card(child: Text("Footer card"))),
],
),
Use RepaintBoundary
com Moderação
Isola repaints: Previne repaints desnecessários na tela, isolando diferentes widgets na sua própria camada. Então, realizando mudanças num componente, o que tiver em volta do RepaintBoundary será cacheado e não será reconstruído.
Melhora o Desempenho da Animação: Ajuda no cache e na renderização mais rápida de subárvores estáticas.
Otimiza o Painting: Limita a área de pintura às partes necessárias, aumentando a eficiência.
Quando vc abre o widget tree, do devtools, e clica num RepaintBoundary, quando vc vai em details, mostra o % de utilidade que ele esta trazendo
Pontos de atenção: o uso excessivo do RepaintBoundary pode trazer um alto consumo de memória, deixar a construção da pagina mais lenta e dificuldade no debug para identificar problemas de performance. Então, use com cuidado.
Use AnimatedBuilder
com child
Quando você usa algum widget que rebuilda muito, use o parâmetro child do builder para evitar o rebuild desnecessário varias vezes.
AnimatedBuilder(
animation: animation,
child: Column(
children: [
Container(
color: Colors.red,
width: 70,
height: 70,
),
],
),
builder: (context, child) => Transform.rotate(
angle: animation.value,
child: child, // -> use o child (parâmetro do builder) aqui
),
),
Minimize Operações Intrínsecas
Quando uma lista precisa pré calcular o height de cada item, isso se chama intrinsic operations.
Para cada item da lista, flutter precisa pre calcular altura e depois repassar item por item, para setar essa altura. Nisso, perdemos a vantagem de lazy builders, ja que o flutter precisará renderizar todos os itens da lista.
A melhor forma de resolver isso é colocando a lista dentro de um widget com um height pre definido (para listas horizontais) ou usando CustomScrollView + SliverList.builder (para listas verticais).
Da pra identificar esses “intrinsic passes” pelo dev tools, na aba performance, selecionando Enhancing tracing > Trace layouts
Depois, clique no frame que esta tendo jank
Em timeline events, procure pela timeline de UI
Aparecerá todos os itens dessa lista construídos na horizontal (sendo que poderia ter criado somente o que esta visível na tela).
No exemplo acima, a pagina está exibindo somente 5 cards, mas o devtools detectou que foram buildados + de 10 cards.
Sempre dê dispose()
em Streams
Quando escutamos uma stream, mesmo se sairmos da page/controller que esse stream esta sendo escutado, GC do flutter não consegue identificar que ela é um recurso não utilizado e não consegue limpar ela da memória. Precisamos fazer isso manualmente.
Então, sempre que usar .listen()
numa stream, é preciso criar uma variável StreamSubscription<T>
e armazenar a chamada stream.listen()
para cancelar ela, quando não precisar mais dos eventos retornados. StreamController
ou qualquer outra classe que herda de Listenable (ou que dê pra usar .listen()
ou addListener()
) precisam ser fechados também.
class StreamSubscriptionExample {
StreamSubscription<dynamic>? _subscription;
void listenToStream(Stream<dynamic> stream) {
_subscription = stream.listen(
(data) {
// Handle stream data
},
onError: (error) {
// Handle error
},
onDone: () {
// Handle stream done
},
);
}
void dispose() {
_subscription?.cancel();
_subscription = null;
}
}
Conclusão
Em resumo, embora o Flutter ofereça uma base poderosa e otimizada para o desenvolvimento de aplicativos, a performance final depende diretamente das escolhas feitas ao longo do desenvolvimento. Aplicar as boas práticas que discutimos aqui é essencial para garantir uma experiência fluida, responsiva e eficiente para o usuário. Cada detalhe conta — desde o uso consciente de widgets até o gerenciamento adequado de streams e renderizações. Se você tiver dúvidas, quiser discutir algum ponto específico ou até compartilhar suas próprias experiências com performance em Flutter, fique à vontade para entrar em contato. Vai ser um prazer trocar ideias!
Referências
Raster thread performance optimization tips
Common mistakes with ListViews in Flutter
Checking for off screen layers
Why 99% of Apps Are Wasting Performance on BuildContext Lookups
Excelente artigo, muito bem estruturado! 👏
Fiquei com uma dúvida sobre o trecho que orienta a armazenar chamadas como Theme.of(context) e MediaQuery.of(context) em variáveis dentro do build().
Mesmo utilizando final, essas variáveis não são reatribuídas toda vez que o build() é chamado? Se for isso, o ganho seria mais em legibilidade do que em performance, certo?
Só queria confirmar se entendi corretamente. Obrigado pelo conteúdo!