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

1. O Que Você Vai Aprender


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.

DART
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
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.

(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

100%
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]
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.

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

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.

: Habilitando análise de desempenho

DART
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
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.
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

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.

: Card de produto otimizado com const

DART
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
  );
}
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.

▶ Exemplo

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.

: RepaintBoundary para isolar repaints

DART
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]),
  ),
)
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.

(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

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.

: Dividindo um Widget grande

DART
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('+')),
    ]);
  }
}
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

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.

: AutomaticKeepAliveClientMixin

DART
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]),
    );
  }
}
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.

▶ Exemplo

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.

: Liberação de recursos em Dispose

DART
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();
  }
}
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.

7. Otimização de Rede

▶ Exemplo

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.

: cached_network_image para cache de imagens

DART
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
)
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.

▶ Exemplo

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.

: API Paginada + pré-carregamento

DART
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]);
      },
    );
  }
}
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.

8. Exemplo Completo: Lista de Produtos de Alto Desempenho do ShopApp

DART
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),
                      ),
                    ],
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }
}
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.

❓ 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


📝 Exercícios

  1. Básico (dificuldade ⭐): Use o Widget Rebuild Tracker no DevTools para encontrar Widgets frequentemente rebuildados, e otimize-os com const.
  2. Intermediário (dificuldade ⭐⭐): Adicione RepaintBoundary + itemExtent a uma lista de produtos, e compare o FPS antes e depois da otimização.
  3. 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.

← Aula Anterior | Próxima Aula →

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%