Elimina los rebuilds inútiles gracias a const, las keys y la reconciliación, y luego mide con DevTools.
Abrir esta lección en KodokonFlutter 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.
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');
}
}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.
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)),
),
],
);
}
}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.
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(),
),
),
),
);
}
}