Performance en Flutter ¿Qué es? ¿Cómo se mide?¿Cómo se interpreta?¿Cómo se mejora?
Una vez estaba viendo Star Wars y Yoda dijo : “Si controlar el performance de Flutter quieres…medirlo debes”
Bueno y ahora…hablando seriamente, Flutter tiene un performance excelente, pero en ocasiones por errores de desarrollo podemos caer accidentalmente en prácticas que impactan el desempeño de nuestra app.
Lo anterior, puede que lo notemos por una latencia a la hora de dibujar elementos en pantalla, pero en muchas otras circunstancias el error no es perceptible al ojo humano. Por ejemplo, si tu aplicación gasta más batería de la esperada, ocupa más espacio del esperado en memoria o tiene ciertos comportamientos extraños ante algunas animaciones; es un indicio que deberías medir el performance de la aplicación.
Así que vamos a navegar en este tema algo profundo 🚤, espero que sea de tu agrado este artículo, no olvides a la final dejarnos unos +50 aplausos y comentarios si te surge alguna inquietud.
¿Qué es el performance?
Debemos comenzar definiendo el tema que estamos analizando. Podemos entender el perfomance como un conjunto de propiedades cuantificables de un ejecutable. En nuestro contexto no es la ejecución de la acción en sí misma, es que tan bien se desempeña al realizarla.
Resalto cuantificables, porque es vital tener un número que represente la habilidad de ejecución de la tarea. Por ejemplo, el tamaño de tu aplicación final, es una medida asignable a un número y de esa forma es claro que si una nueva característica de tu app hace que pase 120 MB de peso a 200 MB es un indicio de que algo puede estar mal y a lo mejor tienes oportunidades de mejora.
Por tanto, todos los elementos para medir el performance deben ser cuantificables.
¿Por qué es importante?
Debemos medir el performance de manera automática para garantizar que nuestras aplicaciones si están dando la mejor experiencia posible a nuestros clientes. En la mayoría de ocasiones un error en el rendimiento de una aplicación está asociado a problemas de implementación no al SDK.
Sin embargo, no todo es visible al ojo humano. Por dar un ejemplo, los FPS es la unidad que mide la cantidad de fotogramas que un dispositivo genera o procesa durante un periodo de tiempo de 1 segundo. La idea es que nuestras aplicaciones funcionen mínimo a 60 FPS. No obstante, el ojo humano solo distingue poco más de 25 FPS, entonces nos sería muy difícil medir un buen funcionamiento utilizando únicamente nuestros sentidos. Un mal tiempo de renderizado implica un gasto innecesario de la GPU y esto incide en un mayor gasto en la batería.
Por tanto, si no medimos nuestro performance, nos dedicaremos a apagar incendios en lugar de garantizar la calidad de nuestros desarrollos desde antes de salir a producción.
Elementos a tener presente
Existe cinco puntos que debes tener en el radar a la hora de medir el performance:
- Debes hacer que las métricas sean fáciles de entender 📈. Siempre debes llevar las mediciones a números y que los desarrolladores puedan entender cuándo es una mejora en el performance o una afectación.
- Realiza métricas poco ambiguas🧐. Con esto me refiero a que se debe establecer una unidad y cuál va a ser el estándar para esa medida. Por ejemplo, todas nuestras aplicaciones deberán funcionar mínimo a 60 FPS (eso no tiene nada de ambiguo ni da lugar a interpretaciones por desarrollador).
- Haz que el performance sea fácil de comparar. Por ejemplo, mantener la medición anterior contra la nueva puede ayudar a identificar mejorías o perjuicios en el rendimiento.
- Debemos hacer que las métricas de rendimiento controlen una población lo más amplia posible, para que nada se quede atrás. De esta forma garantizamos el buen rendimiento de la App.
- No debes realizar optimizaciones de performance hasta que un test te demuestre que en verdad existe un problema. Esto sucede porque en ocasiones tenemos la creencia que ciertos desarrollos pueden mejorar el performance, pero no debemos apresurarnos a corregir supuestas fallas hasta que corroboremos con pruebas que en efecto existe un error en cierta parte de nuestra implementaciones.
Categorías del Performance
Podemos dividir el performance en dos categorías principales a saber tiempo y espacio. En la categoría tiempo podemos ver mediciones como FPS, tiempo hasta primer frame dibujado, etc. Y en la categoría espacio veremos elementos como tamaño de la aplicación, memoria consumida por un proceso, etc.
¿Cómo podemos medir el performance en nuestras aplicaciones Flutter?
Bueno llegamos al momento que muchos estábamos deseosos de llegar y es hora de aplicar la teoría y conocer las herramientas de medición de performance.
En primer lugar debo mencionar un asunto importante Flutter por defecto tiene buen performance, entonces sí notamos algún error en nuestra app muy probablemente el error este de nuestro lado. Para ayudar con este tema nos ha regalado una herramienta poderosa para medir nuestro performance y hasta poder diagnosticar la raíz de nuestro problema. Y esta es el modo profile. Para entrar en este modo puedes ejecutar el siguiente comando :
flutter run --profile
Nota: Este comando se debe ejecutar en un dispositivo físico. Recuerda que para ejecutar este comando debes tener bien configurado tus certificados y los permisos de tu celular.
En caso de que tengas varios dispositivos conectados debes escoger entre ellos el que desees.
Cuando el proceso finaliza podrás observar algo como en la siguiente imagen:
Estas urls te llevarán a dos herramientas: Una es un observatorio establecido sobre el teléfono configurado:
En este observatorio podrás observar el consumo de memoria presente y los picos más altos que se hayan presentado mientras usas la aplicación. Sin embargo, Google te da una segunda herramienta en la cual puedes evaluar varios puntos que pueden ser muy interesantes, por ende vamos a ver el segundo enlace el cual son nuestras conocidas herramientas de desarrollador.
Estas dos herramientas son las que utilizaremos para medir nuestro performance.
¿Cómo se interpreta?
Vamos a poner a prueba estas herramientas con aplicación demo 🧐:
Para ello les he diseñado un pequeño ejemplo, en el cual dibujamos en pantalla diez mil widgets, en un primer escenario con un mal código (Low Perfo) y en un segundo escenario un código con mejores prácticas (High Perfo). También tenemos una pantalla para probar nuestro consumo de internet. Por último, generaremos dos compilaciones y compararemos sus tamaños.
Para empezar veamos los dos códigos. En primer lugar low_perfo.dart:
Como vemos en este código hay varias malas prácticas, por un lado tenemos un Column dibujando 10000 widgets esto hará que no se pueda mostrar ningún elemento en pantalla, hasta que Flutter dibuje estos elementos. Por otro lado, tenemos una función que devuelve widgets y esto también es una mala práctica . En definitiva esta solución debería tener un mal rendimiento, exageré un poco la mala implementación para que sea más evidente el error.
Por otro lado, tenemos la implementación high_perfo.dart:
En esta implementación tenemos un ListView.Builder que nos ayuda a construir todo de forma lazy es decir solo se construye aquellos elementos, con los que esta interactuando el usuario. Por ende, la diferencia versus el código anterior debería ser significativamente evidente. ¿Pero será de esa forma?
Con estos dos códigos listos arranqué mis pruebas de performance:
Veamos en primer lugar lo que arrojó nuestra primera herramienta (el observatorio):
Desde ya podemos ver en el observatorio que hay alto consumo de memoria del dispositivo al entrar a la experiencia en la que deducimos que tendría mal performance. Sin embargo, si queremos ver más detalle sería muy bueno que examináramos las herramientas del desarrollador.
En primer lugar, analicemos la pestaña de performance:
Como vez se muestran en pantalla algunos gráficos, sin embargo ¿Qué significa estos gráficos de barras?
UI
El hilo de UI ejecuta el código Dart en Dart VM. Este hilo genera el layer tree recuerda que Flutter ejecuta en paralelo unos árboles de widgets, elementos y render object, para lograr dibujar en pantalla los elementos. (sugiero que leas el siguiente artículo si no cuentas con este contexto enlace). El layer tree tiene los elementos a dibujar pero no sabe cómo hacerlo, por ello envía su información al hilo raster.
Raster
El hilo de raster toma el layer tree expuesto en el hilo de UI y entonces envía los comandos necesarios a la GPU para lograr dibujar los elementos en pantalla.
Jank (slow frame)
Un frame que se ha demorado mucho en dibujar en pantalla se considera un Jank. Como estándar, cualquiera que demore más de 16 ms (para dispositivos que trabajan a 60 FPS) en terminar su dibujado se considera un Jank.
Shader compilation
Los fotogramas que realizan la compilación de shaders están marcados en rojo oscuro.
Con estos conceptos en mente podemos entender los gráficos arrojados por la pestaña de performance. Como vemos al dibujar en pantalla la experiencia de alto rendimiento no presenta ninguna anomalía a la cual preocuparnos pues ningún frame tarde más de 17 ms para terminar su proceso.
Ahora vamos a ver la experiencia de bajo performance:
Cómo vemos para esta pantalla tenemos varios Jank o frames lentos. En este momento podemos deducir qué fue muy costoso crear el layer tree por tanto el hilo implicado presenta un gran tiempo de ocupación.
Si le damos clic en una de las barras que indica que existe un Jank, se nos muestra la siguiente info :
Como vemos tenemos una línea de tiempo de nuestros eventos, si bien no es muy fácil de entender estos gráficos, podemos ver que en la línea de tiempo lo que más demoró fue la construcción de la pantalla. Por tanto, podemos ver que en el resumen (pestaña Sumary) se presenta 513ms para procesar este frame 🤯.
Normalmente, reviso la pestaña Bottom up para comprender que está sucediendo, así que vamos a darle un vistazo :
Cómo puedes observar tenemos un tiempo de 345 ms de demora en realizar el dibujo en pantalla de los números (textos) y esto sin entrar en detalle en los otros procesos. Esto no es un comportamiento normal, pues Flutter normalmente ejecuta esto de forma eficiente. ¿Entonces donde podría estar el problema? Podemos aplicar un filtro y ocultar todos los elementos relacionados a las librerías core y ver si encontramos algún problema en nuestros desarrollos.
Como vemos en pantalla podemos identificar que los procesos más pesados están relacionados a nuestros propios desarrollos, en este caso en el proceso de construcción de la pantalla y dibujado de los componentes de la lista. Esto indica que tenemos un error en nuestra desarrollo que en total está gastando (219+219+ 73) ms esto quiere decir que de los 513ms, 511ms están utilizando en dibujar nuestros desarrollos 😂.
También puedes habilitar el Performance Overlay desde las herramientas de desarrollador esto mostrará sobre el celular el consumo del hilo Raster y UI.
Midamos el consumo de CPU
Aún podemos aprovechar este ejemplo para analizar las dos pestañas siguientes, comencemos por el CPU profiler. El cual nos dirá cuánto es el desgaste computacional para nuestra solución.
Este implica grabar un periodo de ejecución y nos mostrará los resultados del análisis:
Como podemos ver, ningún proceso de ejecución de la pestaña de alto performance está relacionado a los desarrollos implementados, en cambio en la pantalla de bajo performance los procesos más demorados están relacionados a la construcción de la pantalla de bajo performance.
Midamos el consumo de memoria
Por otro lado, está la pestaña de memoria (Memory) veamos que nos arroja el análisis:
Como vemos, el consumo de memoria se eleva en el momento de construir la pantalla, pero una vez ha finalizado el consumo disminuye, esto ocurre en la experiencia de bajo performance. Pero podemos observar que en la experiencia de alto performance no se evidencia un pico de memoria al momento de dibujar la experiencia.
Esto nos muestra que tenemos problemas en el momento de construcción de la pantalla de bajo performance.
Midamos el consumo de internet
Bueno pasemos a la pestaña de viaje de datos a internet, esto es una pantalla que nos permite medir los tiempos de respuesta, tipo de datos, respuestas entre otros elementos interesantes:
Como puedes ver esto nos arroja todos los consumos que tenemos a internet y en caso de hacer peticiones innecesarios o presentar altos tiempos de respuesta de parte de algunos de los servicios será evidenciado en este reporte.
Se puede validar información que puede ser relevante para nuestros desarrollos.
Midamos el consumo de espacio de almacenamiento
Antes de salir a producción es bueno analizar el tamaño ocupado por nuestras aplicaciones, para ello podemos valernos de las herramientas disponibles. En este caso la pestaña App Size.
Para analizar el tamaño de tu aplicación podemos ejecutar los siguientes comandos:
flutter build apk --analyze-size
flutter build appbundle --analyze-size
flutter build ios --analyze-size
flutter build linux --analyze-size
flutter build macos --analyze-size
flutter build windows --analyze-size
Esto generará un archivo json, te recomiendo siempre conservar el último json generado pues como veras en breve puedes comparar entre dos análisis.
En este escenario generé dos reportes uno con una aplicación con un vídeo dentro de sus assets y otro sin él. Mi idea es que esta pestaña nos indique dónde se presentó el aumento del espacio de nuestra app.
En la primera sección de análisis nos permite ver en detalle cuales son los consumos de espacio de almacenamiento que está haciendo nuestra aplicación. Entre los dos reportes se ve una diferencia notable, pues una aplicación pesa 54 MB y la otra 68.3 MB lo que indica que algo pasó en la última versión que hizo que subiera el peso de nuestra App. Pero, ¿Cómo validamos que fue lo que ocurrió? para ello utilizaremos la pestaña Diff:
En este caso cómo se ve en la imagen izquierda, en primer lugar se suben los dos reportes y luego de dar clic en analizar diferencia se evidencia que el causante de este aumento en el tamaño de la aplicación es un vídeo ubicado dentro de la carpeta imágenes llamado video.mov 🤯.
Esta herramienta también nos permite observar si disminuimos el tamaño de nuestra app. Por tanto, resulta muy útil realizar estos análisis en nuestro proceso previo al despliegue a producción.
¿Cómo se mejora el performance?
En este momento ya fueron muchos elementos que analizamos, solo quedaría faltando por resolver lo siguiente ¿Cómo arreglamos nuestras fallas en rendimiento?
Debemos pensar en qué estamos haciendo en nuestra experiencia e identificar cuáles son las mejores prácticas de desarrollo para abordar este tipo de solución eso si siempre buscando en la documentación oficial.
Por ejemplo, pensemos en la pantalla Low Perfo, evidentemente tenemos un problema allí. Según lo arrojado en nuestro análisis el problema recae en el dibujado de nuestra pantalla. Pero, ¿qué estamos dibujando allí? Una lista larga de widgets.
En la documentación oficial existe un apartado donde nos dice cómo podemos trabajar nuestras listas de la mejor forma posible (enlace)(enlace 2). Si seguimos los consejos suministrados en estos enlaces sin lugar a duda mejorará nuestro rendimiento.
Por tanto, cuando deseemos mejorar el performance debemos meditar en qué estamos haciendo y contrastarlo contra la documentación oficial.
Conclusión
Espero que haya sido de tu agrado este artículo. Perdona un poco lo extenso pero dado el detalle del tema era necesario abarcar varios puntos. Si el artículo te gusto recuerda dejar +50 👏 en señal de gratitud.
Estoy convencido que con esta herramienta podrás identificar las posibles fallas, de tu app y atacar los problemas que evidencies. Pero … ¡¡espera!! No te conté que hay una forma de medir algunos de estos elementos desde procesos automáticos. Síguenos para ver la siguiente parte de este artículo donde les contaré cómo lo podemos hacer 🧑🏻💻. También te dejo el enlace al repositorio del ejemplo (enlace repo).