Kodokon kodokon.com

Rendimiento: const, rebuilds, keys y DevTools

Elimina los rebuilds inútiles gracias a const, las keys y la reconciliación, y luego mide con DevTools.

10 min · 3 preguntas

Abrir esta lección en Kodokon

Flutter maneja tres árboles: los widgets (configuraciones inmutables y desechables), los Element (instancias vivas que llevan el estado) y los render objects (layout, pintado). Reconstruir un widget es barato; lo que cuesta es el layout y el pintado en cascada. Durante un rebuild, Element.updateChild aplica tres reglas en orden: si el nuevo widget es idéntico (la misma instancia) al antiguo, se cortocircuita todo el subárbol; si Widget.canUpdate es true (mismo runtimeType, misma key), se conserva el Element y simplemente se reconfigura; en caso contrario, el Element se destruye y se vuelve a inflar, estado incluido. La palabra clave const aprovecha la primera regla: el compilador canonicaliza una expresión constante, la misma instancia vuelve en cada build, identical es true, y toda la rama se omite.

DART
import 'package:flutter/material.dart';

class CounterScreen extends StatefulWidget {
  const CounterScreen({super.key});

  @override
  State<CounterScreen> createState() =>
      _CounterScreenState();
}

class _CounterScreenState extends State<CounterScreen> {
  int _count = 0;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        const _Header(),
        Text('Total: $_count'),
        FilledButton(
          onPressed: () => setState(() => _count++),
          child: const Text('Increment'),
        ),
      ],
    );
  }
}

class _Header extends StatelessWidget {
  const _Header();

  @override
  Widget build(BuildContext context) {
    debugPrint('build _Header');
    return const Text('Counter');
  }
}
El log aparece una sola vez: const omite el subárbol.

En las listas de hijos, la reconciliación empareja primero por posición. Reordena elementos con estado sin keys y cada Element conserva el estado del ocupante anterior de su posición: casillas marcadas en el lugar equivocado, campos de texto intercambiados. Una Key cambia la regla de emparejamiento: el algoritmo encuentra el Element que lleva la misma key, aunque se haya movido dentro de la lista. Usa ValueKey sobre un identificador de negocio estable. GlobalKey va más lejos, permite reparentar un subárbol entero sin perder su estado, pero tiene un coste real (registro global, restricción de unicidad en todo el árbol): resérvala para necesidades reales como Form o el reparentado.

DART
import 'package:flutter/material.dart';

class TodoList extends StatelessWidget {
  const TodoList({super.key, required this.items});

  final List<String> items;

  @override
  Widget build(BuildContext context) {
    return ListView(
      children: [
        for (final item in items)
          Dismissible(
            key: ValueKey(item),
            onDismissed: (direction) {},
            child: ListTile(title: Text(item)),
          ),
      ],
    );
  }
}
Sin una key estable, Dismissible lanza un error en cuanto eliminas.

Mide antes de optimizar. En DevTools, la vista Performance traza el tiempo por frame de los dos hilos críticos: UI (tu código Dart: build, layout) y raster (la rasterización de las capas). El presupuesto es de unos 16 ms a 60 Hz. El jank del lado raster no se arregla optimizando tus builds: proviene del coste del pintado (sombras, saveLayer, imágenes sobredimensionadas). Activa Track widget builds para ver quién reconstruye, y debugRepaintRainbowEnabled para visualizar las zonas repintadas: el borde cambia de color en cada repintado. RepaintBoundary aísla un subárbol en su propia capa: un spinner que se repinta continuamente ya no ensucia a sus vecinos.

DART
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

void main() {
  debugRepaintRainbowEnabled = true;
  runApp(const DemoApp());
}

class DemoApp extends StatelessWidget {
  const DemoApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      showPerformanceOverlay: true,
      home: Scaffold(
        body: Center(
          child: RepaintBoundary(
            child: CircularProgressIndicator(),
          ),
        ),
      ),
    );
  }
}
Overlay de frames + repaint rainbow: el dúo de diagnóstico.

Prueba de conocimientos

Comprueba que has retenido los puntos clave de esta lección.

  1. ¿Por qué se ignora un widget const cuando su padre se reconstruye?
    • La instancia es canónica: identical es true, y Element.updateChild cortocircuita todo el subárbol
    • El compilador elimina el widget del árbol en release
    • Los widgets const no tienen método build
  2. Reordenas una lista de widgets con estado sin keys. ¿Qué ocurre?
    • Los Elements se emparejan por posición: cada elemento hereda el estado del ocupante anterior de su posición
    • Flutter lanza una excepción en modo debug
    • El estado sigue automáticamente a cada widget movido
    • La lista se niega a reconstruirse
  3. ¿Qué miden las dos gráficas del performance overlay?
    • El tiempo por frame del hilo UI (código Dart) y del hilo raster (renderizado de las capas)
    • La memoria asignada y el número de widgets
    • El tiempo de build y el tiempo de recolección de basura