Saltar al contenido principal

Buenas prácticas de rendimiento

Cómo asegurar que tu aplicación Flutter sea de alto rendimiento.

Generalmente, las aplicaciones Flutter son de alto rendimiento por defecto, por lo que solo necesitas evitar los errores comunes para obtener un rendimiento excelente. Estas recomendaciones de mejores prácticas te ayudarán a escribir la aplicación Flutter más eficiente posible.

¿Cómo diseñas una aplicación Flutter para renderizar tus escenas de la manera más eficiente? En particular, ¿cómo te aseguras de que el código de pintura generado por el framework sea lo más eficiente posible? Se sabe que algunas operaciones de renderizado y diseño son lentas, pero no siempre se pueden evitar. Deben usarse con prudencia, siguiendo la guía a continuación.

Minimizar las operaciones costosas

#

Algunas operaciones son más costosas que otras, lo que significa que consumen más recursos. Obviamente, solo deseas utilizar estas operaciones cuando sea necesario. La forma en que diseñas e implementas la interfaz de usuario de tu aplicación puede tener un gran impacto en su eficiencia.

Controlar el costo de build()

#

Aquí hay algunas cosas a tener en cuenta al diseñar tu interfaz de usuario:

  • Evita el trabajo repetitivo y costoso en los métodos build() ya que build() puede invocarse con frecuencia cuando los widgets ancestros se reconstruyen.
  • Avoid overly large single widgets with a large build() function. Split them into different widgets based on encapsulation but also on how they change:
    • Cuando se llama a setState() en un objeto State, todos los widgets descendientes se reconstruyen. Por lo tanto, localiza la llamada a setState() en la parte del subárbol cuya interfaz de usuario realmente necesita cambiar. Evita llamar a setState() en la parte superior del árbol si el cambio está contenido en una parte pequeña del árbol.
    • El recorrido para reconstruir todos los descendientes se detiene cuando se vuelve a encontrar la misma instancia del widget hijo que en el fotograma anterior. Esta técnica se utiliza mucho dentro del framework para optimizar animaciones en las que la animación no afecta al subárbol hijo. Consulta el patrón TransitionBuilder y el código fuente de SlideTransition, que utiliza este principio para evitar la reconstrucción de sus descendientes al animar. ("Misma instancia" se evalúa utilizando el operator ==, pero consulta la sección de errores comunes al final de esta página para obtener consejos sobre cuándo evitar la sobreescritura del operator ==.)
    • Usa constructores const en los widgets tanto como sea posible, ya que permiten a Flutter acortar la mayor parte del trabajo de reconstrucción. Para recibir recordatorios automáticos para usar const cuando sea posible, habilita los lints recomendados del paquete flutter_lints. Para obtener más información, consulta la guía de migración de flutter_lints.
    • Para crear piezas reutilizables de interfaces de usuario, prefiere usar un StatelessWidget en lugar de una función.

Para obtener más información, consulta:


Usar StringBuffer para una construcción de cadenas eficiente

#

Cuando necesitas construir una cadena a partir de varias partes, especialmente dentro de un bucle, el uso del operador + puede ser ineficiente porque crea un nuevo objeto String en cada concatenación. Un mejor enfoque es usar StringBuffer, que recopila todas las cadenas y las concatena una sola vez cuando llamas a toString().

Ver en YouTube en una nueva pestaña: "StringBuffer (Technique of the Week)"


Usar saveLayer() con prudencia

#

Algunos códigos de Flutter utilizan saveLayer(), una operación costosa, para implementar varios efectos visuales en la interfaz de usuario. Incluso si tu código no llama explícitamente a saveLayer(), otros widgets o paquetes que utilices podrían llamarlo detrás de escena. Tal vez tu aplicación esté llamando a saveLayer() más de lo necesario; las llamadas excesivas a saveLayer() pueden causar tirones (jank).

¿Por qué es costoso saveLayer?

#

Llamar a saveLayer() asigna un búfer fuera de pantalla y dibujar contenido en el búfer fuera de pantalla podría desencadenar un cambio de objetivo de renderizado. La GPU quiere funcionar a máxima capacidad, y un cambio de objetivo de renderizado obliga a la GPU a redirigir ese flujo temporalmente y luego volver a dirigirlo de nuevo. En las GPU móviles, esto es particularmente perjudicial para el rendimiento de renderizado.

¿Cuándo se requiere saveLayer?

#

En tiempo de ejecución, si necesitas mostrar dinámicamente varias formas provenientes de un servidor (por ejemplo), cada una con cierta transparencia, que podrían (o no) superponerse, entonces prácticamente tienes que usar saveLayer().

Depurar llamadas a saveLayer

#

¿Cómo puedes saber con qué frecuencia tu aplicación llama a saveLayer(), ya sea directa o indirectamente? El método saveLayer() desencadena un evento en la línea de tiempo de DevTools; descubre cuándo tu escena utiliza saveLayer marcando el interruptor PerformanceOverlayLayer.checkerboardOffscreenLayers en la vista de Rendimiento de DevTools.

Minimizar llamadas a saveLayer

#

¿Puedes evitar las llamadas a saveLayer? Podría requerir repensar cómo creas tus efectos visuales:

  • Si las llamadas provienen de tu código, ¿puedes reducirlas o eliminarlas? Por ejemplo, tal vez tu interfaz de usuario superpone dos formas, cada una con una transparencia distinta de cero:

    • Si siempre se superponen en la misma cantidad, de la misma manera, con la misma transparencia, puedes precalcular cómo se ve este objeto semi-transparente y superpuesto, guardarlo en caché y usarlo en su lugar en lugar de llamar a saveLayer(). Esto funciona con cualquier forma estática que puedas precalcular.
    • ¿Puedes refactorizar tu lógica de pintura para evitar las superposiciones por completo?
  • Si las llamadas provienen de un paquete que no te pertenece, comunícate con el propietario del paquete y pregúntale por qué son necesarias estas llamadas. ¿Se pueden reducir o eliminar? Si no es así, es posible que debas buscar otro paquete o escribir el tuyo propio.

Otros widgets que podrían activar saveLayer() y son potencialmente costosos:

  • ShaderMask
  • ColorFilter
  • Chip—podría activar una llamada a saveLayer() si disabledColorAlpha != 0xff
  • Text—podría activar una llamada a saveLayer() si hay un overflowShader

Minimizar el uso de opacidad y recorte

#

La opacidad es otra operación costosa, al igual que el recorte (clipping). Aquí tienes algunos consejos que podrían resultarte útiles:

  • Usa el widget Opacity solo cuando sea necesario. Consulta la sección Imagen transparente en la página de la API de Opacity para ver un ejemplo de cómo aplicar opacidad directamente a una imagen, lo cual es más rápido que usar el widget Opacity.
  • En lugar de envolver formas simples o texto en un widget Opacity, generalmente es más rápido simplemente dibujarlos con un color semitransparente. (Aunque esto solo funciona si no hay partes superpuestas en la forma que se va a dibujar).
  • Para implementar el desvanecimiento (fading) en una imagen, considera usar el widget FadeInImage, que aplica una opacidad gradual utilizando el fragment shader de la GPU. Para obtener más información, consulta la documentación de Opacity.
  • El recorte (Clipping) no llama a saveLayer() (a menos que se solicite explícitamente con Clip.antiAliasWithSaveLayer), por lo que estas operaciones no son tan costosas como Opacity, pero el recorte sigue siendo costoso, así que úsalo con precaución. Por defecto, el recorte está deshabilitado (Clip.none), por lo que debes habilitarlo explícitamente cuando sea necesario.
  • Para crear un rectángulo con esquinas redondeadas, en lugar de aplicar un rectángulo de recorte, considera usar la propiedad borderRadius que ofrecen muchas de las clases de widgets.

Implementar cuadrículas y listas con prudencia

#

La forma en que se implementan tus cuadrículas y listas podría estar causando problemas de rendimiento en tu aplicación. Esta sección describe una mejor práctica importante al crear cuadrículas y listas, y cómo determinar si tu aplicación utiliza pasos de diseño excesivos.

¡Sé perezoso (lazy)!

#

Al construir una cuadrícula o lista grande, utiliza los métodos de constructor perezoso (lazy builder) con devoluciones de llamada (callbacks). Eso garantiza que solo se construya la parte visible de la pantalla al momento del inicio.

Para obtener más información y ejemplos, consulta:

Evitar intrínsecos (intrinsics)

#

Para obtener información sobre cómo los pasos intrínsecos podrían estar causando problemas con tus cuadrículas y listas, consulta la siguiente sección.


Minimizar los pasos de diseño causados por operaciones intrínsecas

#

Si has programado bastante en Flutter, probablemente estés familiarizado con cómo funcionan el diseño y las restricciones al crear tu interfaz de usuario. Es posible que incluso hayas memorizado la regla básica de diseño de Flutter: Las restricciones bajan. Los tamaños suben. El padre establece la posición.

Para algunos widgets, en particular cuadrículas y listas, el proceso de diseño puede ser costoso. Flutter se esfuerza por realizar solo un paso de diseño sobre los widgets pero, a veces, se necesita un segundo paso (llamado un paso intrínseco), y eso puede ralentizar el rendimiento.

¿Qué es un paso intrínseco?

#

Un paso intrínseco ocurre cuando, por ejemplo, deseas que todas las celdas tengan el tamaño de la celda más grande o más pequeña (o algún cálculo similar que requiera consultar a todas las celdas).

Por ejemplo, considera una gran cuadrícula de Cards. Una cuadrícula debe tener celdas de tamaño uniforme, por lo que el código de diseño realiza un paso, comenzando desde la raíz de la cuadrícula (en el árbol de widgets), pidiendo a cada tarjeta de la cuadrícula (no solo a las tarjetas visibles) que devuelva su tamaño intrínseco—el tamaño que prefiere el widget, asumiendo que no hay restricciones. Con esta información, el framework determina un tamaño de celda uniforme, y vuelve a visitar todas las celdas de la cuadrícula por segunda vez, diciéndole a cada tarjeta qué tamaño usar.

Depurar pasos intrínsecos

#

Para determinar si tienes pasos intrínsecos excesivos, habilita la opción Track layouts en DevTools (deshabilitada por defecto), y observa la traza de la pila (stack trace) de la aplicación para saber cuántos pasos de diseño se realizaron. Una vez que habilitas el seguimiento, los eventos intrínsecos de la línea de tiempo se etiquetan como '$runtimeType intrinsics'.

Evitar pasos intrínsecos

#

Tienes un par de opciones para evitar el paso intrínseco:

  • Establecer las celdas a un tamaño fijo por adelantado.
  • Elige una celda en particular para que sea la celda "ancla"—todas las celdas se dimensionarán en relación con esta celda. Escribe un RenderObject personalizado que posicione primero el ancla secundaria (child anchor) y luego distribuya los otros elementos hijos a su alrededor.

Para profundizar aún más en cómo funciona el diseño, consulta la sección de diseño y renderizado en la descripción general de la arquitectura de Flutter.


Compilar y mostrar fotogramas en 16ms

#

Dado que hay dos hilos separados para compilar y renderizar, tienes 16 ms para compilar, y 16 ms para renderizar en una pantalla de 60 Hz. Si la latencia es una preocupación, compila y muestra un fotograma en 16 ms o menos. Ten en cuenta que esto significa compilar en 8 ms o menos, y renderizar en 8 ms o menos, para un total de 16 ms o menos.

Si tus fotogramas se están renderizando en un tiempo muy inferior a 16 ms en total en el profile mode, es probable que no tengas que preocuparte por el rendimiento incluso si se aplican algunos errores de rendimiento comunes, pero aun así deberías intentar compilar y renderizar un fotograma lo más rápido posible. ¿Por qué?

  • Reducir el tiempo de renderizado del fotograma por debajo de 16 ms podría no marcar una diferencia visual, pero mejora la duración de la batería y los problemas térmicos.
  • Puede que funcione bien en tu dispositivo, pero considera el rendimiento para el dispositivo de gama más baja al que te diriges.
  • A medida que los dispositivos de 120 fps estén más disponibles, querrás renderizar fotogramas en menos de 8 ms (en total) para proporcionar la experiencia más fluida.

Si te preguntas por qué 60 fps conducen a una experiencia visual fluida, mira el video ¿Por qué 60 fps?

Errores comunes (Pitfalls)

#

Si necesitas ajustar el rendimiento de tu aplicación, o tal vez la interfaz de usuario no es tan fluida como esperas, ¡la vista de Rendimiento de DevTools puede ayudarte!

Además, el plugin de Flutter para tu IDE podría ser útil. En la ventana Flutter Performance, habilita la casilla de verificación Show widget rebuild information. Esta función te ayuda a detectar cuándo los fotogramas se están renderizando y mostrando en más de 16 ms. Cuando sea posible, el plugin proporciona un enlace a un consejo relevante.

Los siguientes comportamientos podrían afectar negativamente el rendimiento de tu aplicación.

  • Evita usar el widget Opacity, y evítalo particularmente en una animación. Usa AnimatedOpacity o FadeInImage en su lugar. Para obtener más información, consulta Consideraciones de rendimiento para la animación de opacidad.

  • Al usar un AnimatedBuilder, evita colocar un subárbol en la función builder que construye widgets que no dependen de la animación. Este subárbol se reconstruye para cada tick de la animación. En su lugar, construye esa parte del subárbol una vez y pásala como un hijo a AnimatedBuilder. Para obtener más información, consulta Optimizaciones de rendimiento.

  • Evita el recorte (clipping) en una animación. Si es posible, realiza el recorte previo de la imagen antes de animarla.

  • Evita el uso de constructores con una List concreta de hijos (como Column() o ListView()) si la mayoría de los hijos no son visibles en la pantalla para evitar el costo de construcción.

  • Evita sobreescribir el operator == en los objetos Widget. Aunque podría parecer que ayudaría a evitar reconstrucciones innecesarias, en la práctica perjudica el rendimiento porque da como resultado un comportamiento O(N²). La única excepción a esta regla son los widgets de hoja (widgets sin hijos), en el caso específico en el que comparar las propiedades del widget probablemente sea significativamente más eficiente que reconstruir el widget y donde el widget rara vez cambiará de configuración. Incluso en tales casos, generalmente es preferible confiar en el almacenamiento en caché de los widgets, porque incluso una sola sobreescritura de operator == puede resultar en una degradación general del rendimiento, ya que el compilador ya no puede asumir que la llamada es siempre estática.

Recursos

#

Para obtener más información sobre el rendimiento, consulta los siguientes recursos: