Elementos, llaves y rendimiento de Flutter.

Cesar Vega
15 junio, 2019

. . .

Este artículo es una traducción de un artículo publicado originalmente en inglés por Tomek Polańskien Medium. Por favor, visita el siguiente enlace y recomienda el artículo original si te gusta el contenido:

TL;DR: Widget Keyspuede mejorar el rendimiento de nuestra aplicación en lugares donde no se obtienen los 60 FPS prometidos.


Un Elemento es creado internamente por un Widget. Su objetivo principal es saber en qué parte del árbol de widgets se encuentra el widget que lo creó.

Los Elements son costosos de crear y si es posible, deben ser reutilizados. Esto se puede conseguir con las teclas ( ValueKeys y GlobalKeys).

Ciclo de vida del elemento

  • Mount — Se llama cuando el elemento se agrega al árbol por primera vez.
  • Activate — Se llama cuando se activa un elemento previamente desactivado.
  • Update — Actualiza el RenderObjectcon nuevos datos.
  • Deactivate — Se llama cuando el Elementes removido/movido del árbol de Widgets. Un Elementpuede ser reactivado si fue movido durante el mismo Frame y tiene una GlobalKey.
  • Unmount — Si el Elementno fue reactivado durante un Frame, será desmontado y no podrá ser reutilizado más.

Para mejorar el rendimiento necesitamos utilizar activar y actualizar tan a menudo como sea posible y tratar de evitar que se dispare mount y Unmount.

IMPORTANTE: La mayoría de las veces no necesitas una optimización especial ya que Flutter es rápido, lo que siguiente a continuación, es lo que te recomendaría usar cuando quieras arreglar un problema de rendimiento visible.

Cambio de posición en un Column/Row

Para el primer ejemplo, queremos cambiar el orden de visualización del Containerrojo y del Placeholder:

Widget build(BuildContext context) {
return Column(
children: [
value
? const SizedBox()
: const Placeholder(),
GestureDetector(
onTap: () {
setState(() {
value = !value;
});
},
child: Container(
width: 100,
height: 100,
color: Colors.red,
),
),
!value
? const SizedBox()
: const Placeholder(),
],
);
}

Esto es lo que sucede cuando pulsamos en el GestureDetector[1]:

08:17:53.652: update Column
08:17:53.666: deactivate Placeholder
08:17:53.679: deactivate GestureDetector
08:17:53.679: deactivate Container
08:17:53.679: deactivate SizedBox
08:17:53.679: mount SizedBox
08:17:53.679: mount GestureDetector
08:17:53.679: mount Container
08:17:53.691: mount Placeholder
08:17:53.691: unmount SizedBox
08:17:53.698: unmount Placeholder
08:17:53.700: unmount Container
08:17:53.715: unmount GestureDetector

Como puedes ver, sólo se actualizó Columny el resto de los elementos se desactivaron primero, se montaron los nuevos Elementsy luego se eliminaron los antiguos.

Veamos cuánto tiempo se tardó en reconstruir ese elemento — para ello, utilizo la función Timeline del Observatorio.

En este gráfico se pueden ver pares de líneas de tiempo:

  • Mount <nombre del widget> — p.e. Mount Placeholder — este es el tiempo que tardó la fase de montaje
  • <Nombre del widget> — p.e. Placeholder— este es el tiempo que tardó la construcción del widget

En promedio, la construcción de todos esos widgets tomó 5.5ms

¿Cómo mejorar esto?

Puedes mejorar esto asignando ValueKeys a los widgets raíz que se están desmontando:

Widget build(BuildContext context) {
return Column(
children: [
value
? const SizedBox(key: ValueKey('SizedBox'))
: const Placeholder(key: ValueKey('Placeholder')),
GestureDetector(
key: ValueKey('GestureDetector'),
onTap: () {
setState(() {
value = !value;
});
},
child: Container(
width: 100,
height: 100,
color: Colors.red,
),
),
!value
? const SizedBox(key: ValueKey('SizedBox'))
: const Placeholder(key: ValueKey('Placeholder')),
],
);
}

Ahora tenemos esta salida de registro:

Ahora tenemos esta salida de registro:

08:21:37.576: update Column
08:21:37.594: update SizedBox-[<’SizedBox’>]
08:21:37.596: update GestureDetector-[<’GestureDetector’>]
08:21:37.611: update Container
08:21:37.619: update Placeholder-[<’Placeholder’>]

La salida de la línea de tiempo se ve así:

Tanto en la salida del registro como en la línea de tiempo no se ve ningún montaje. Además, el tiempo medio de construcción de los Widgets es de 1,6ms en comparación con los 5,5ms [2] anteriores.

Cambiar un padre de un widget

A veces te gustaría centrar tu Widget en una pantalla, pero cuando no hay suficiente espacio, te gustaría poner nuestro Widget en un SingleChildScrollViewpara que no se desborde.

En esos casos, necesitas cambiar la posición de tu Widgeten el árbol de Widgets:

Widget build(BuildContext context) {
final inner = MaterialApp(
home: Container(
width: 100,
height: 100,
color: Colors.red,
),
);
return GestureDetector(
onTap: () {
setState(() {
value = !value;
});
},
child: value ? SizedBox(child: inner) : inner,
);
}

En este ejemplo, dependiendo del value, rodeamos un widget MaterialAppbastante complejo con un widget SizedBox.

Veamos la salida del registro [1]:

09:41:43.325: update GestureDetector
09:41:43.348: deactivate MaterialApp
09:41:43.350: deactivate Container
09:41:43.352: mount SizedBox
09:41:43.352: mount MaterialApp
09:41:43.425: mount Container
09:41:43.450: unmount Container
09:41:43.476: unmount MaterialApp

El tiempo medio de construcción es de 67 ms [2].

¿Cómo mejorar esto?

Para reutilizar el widget MaterialAppnecesitamos asignarle una GlobalKey(un ValueKeynormal no es suficiente)

class _GlobalKeyWidgetState extends State<GlobalKeyWidget> {
bool value = false;
final global = GlobalKey();

@override
Widget build(BuildContext context) {
final inner = MaterialApp(
key: global,
home: Container(
width: 100,
height: 100,
color: Colors.red,
),
);
return GestureDetector(
onTap: () {
setState(() {
value = !value;
});
},
child: value ? SizedBox(child: inner) : inner,
);
}
}

La salida del registro en la línea de tiempo es la siguiente:

09:56:46.993: update GestureDetector
09:56:47.030: deactivate MaterialApp
09:56:47.060: deactivate Container
09:56:47.060: mount SizedBox
09:56:47.072: activate MaterialApp
09:56:47.095: activate Container
09:56:47.098: update MaterialApp
09:56:47.188: update Container

De nuevo, tanto en la salida de registro como en la línea de tiempo, no se ve el montaje para MaterialApp, sólo para SizedBoxque se ha añadido recientemente. El tiempo medio de construcción de los Widgets es de 25ms comparado con los 67ms [2] anteriores.

¿Qué hay acerca de los Slivers?

¿Cómo evitar el montaje de un nuevo Elementfrente a la reutilización del existente?

Los mismos tipos de Widgets

Cada vez que cambie el orden de los elementos de una lista y esos elementos sean del mismo tipo, esos elementos se reutilizarán:

Widget build(BuildContext context) {
return Directionality(
textDirection: TextDirection.ltr,
child: GestureDetector(
onTap: () {
setState(() {
value = !value;
});
},
child: ListView(
children: <Widget>[
value ? Placeholder(color: Colors.red) : Placeholder(),
!value ? Placeholder(color: Colors.red) : Placeholder(),
],
),
),
);
}

Sin montaje adicional

Diferentes tipos de Widgets

Cuando tienes diferentes tipos de widgets y cambias el orden, los Elementsdetrás de esos Widgetsserán recreados y montados:

Widget build(BuildContext context) {
return Directionality(
textDirection: TextDirection.ltr,
child: GestureDetector(
onTap: () {
setState(() {
value = !value;
});
},
child: ListView(
children: <Widget>[
value
? Placeholder(color: Colors.red)
: Container(height: 100),
!value
? Placeholder(color: Colors.red)
: Container(height: 100),
],
),
),
);
}

Desafortunadamente, el uso de LocalKeysno solucionará este problema. El uso de claves globales soluciona este problema, pero como ya he mencionado anteriormente, el mal uso de GlobalKeyspuede causar otros problemas.

Gracias a boformer por señalar esto!

Conclusión

El rendimiento de Flutter en la mayoría de los casos es suficientemente bueno y no requiere micro-optimizaciones. Por otro lado, hay casos en los que necesitamos hacer más trabajo para que nuestras aplicaciones funcionen a 60 FPS.

La desventaja de usar ValueKeyses que abultan nuestro código y con GlobalKeypodríamos tener algunos errores si los duplicamos.

Utilizado de forma responsable, las Keys pueden ayudarte a llevar el rendimiento de tu aplicación justo donde quieres.

El código fuente se puede encontrar aquí.

Si quieres saber más sobre Elementsconsulta el artículo de Norbert.


[1] He eliminado, por simplicidad del registro, la salida de los elementos que se crean internamente

[2] Esas mediciones se hicieron en una depuración basada en un emulador — en un release construido en un dispositivo, esas serían mucho más pequeñas.

0

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Comunidades en Español