Cuando nos lanzamos a desarrollar con Jetpack Compose, es muy fácil dejarse llevar por la magia de la interfaz declarativa. Sin embargo, si no tenemos cuidado, podemos acabar con una aplicación que se siente pesada o que da tirones molestos. La clave para que todo vaya como la seda reside en dominar la recomposición, ese proceso donde Compose decide qué partes de la pantalla deben actualizarse cuando los datos cambian.
Optimizar el rendimiento no es cuestión de intuición o de cambiar código a ciegas, sino de aplicar un enfoque científico. Para conseguir una experiencia de usuario realmente fluida, necesitamos entender que no todo el código se ejecuta de la misma manera y que existen trucos específicos para evitar que el procesador trabaje de más en tareas que no aportan valor visual inmediato.
El ciclo de vida del renderizado en Compose
Para no dar palos de ciego, primero hay que pillar cómo Compose dibuja la interfaz. Todo ocurre en tres etapas: primero la fase de composición, donde se define qué elementos se muestran y se crea el árbol de nodos. Luego viene la fase de diseño (layout), que se encarga de medir los componentes y decirles exactamente dónde colocarse en la pantalla. Finalmente, llegamos a la fase de renderización, que es cuando los píxeles se dibujan físicamente.
El problema es que la composición es, con diferencia, la etapa más costosa. Si un estado cambia y provoca que todo el árbol se regenere, la app se volverá lenta. El secreto de los profesionales es aislar el estado para que solo se recompongan los componentes que estrictamente necesitan el nuevo valor, evitando que la cascada de actualizaciones llegue a partes de la UI que no han variado.
Estrategias para reducir el trabajo del procesador
Una de las trampas más comunes es realizar cálculos pesados directamente en el cuerpo de un composable. Como estas funciones pueden ejecutarse muchísimas veces (¡incluso en cada fotograma de una animación!), debemos usar remember para cachear los resultados. Si tienes una lista que necesita ordenarse, no lo hagas dentro del LazyColumn; hazlo fuera y guárdalo en memoria para que solo se recalcule cuando los datos de origen cambien realmente.
Cuando trabajamos con listas, es vital asignar claves estables (keys) a los elementos. Sin ellas, si un ítem se mueve de posición, Compose podría pensar que todos los elementos siguientes han cambiado y recomponerlos todos. Con una clave única, el sistema entiende que el elemento simplemente se ha desplazado, ahorrando tiempo de ejecución considerablemente, optimizando la creación de listas eficientes con LazyColumn.
El poder de derivedStateOf y la lectura diferida
A veces, un estado cambia tan rápido que dispara recomposiciones sin sentido. Un ejemplo típico es el desplazamiento de una lista. Si queremos mostrar un botón de «volver arriba» solo cuando el usuario ha bajado un poco, no nos interesa saber cada píxel que se mueve, sino solo si el índice del primer elemento visible es mayor a cero. Aquí es donde derivedStateOf entra al rescate, ya que actúa como un filtro que solo avisa a Compose cuando el resultado de la condición cambia, reduciendo drásticamente la frecuencia de actualizaciones.
Otra técnica avanzada es aplazar las lecturas de estado. En lugar de pasar un valor directamente, podemos pasar una lambda. De este modo, la lectura del valor no ocurre en la fase de composición, sino que se pospone. Si usamos modificadores basados en lambda (como offset { ... } en lugar de offset(dp)), podemos saltarnos la fase de composición y de diseño, y pasar directamente al dibujo, lo que es un auténtico regalo para el rendimiento de la batería y la fluidez.
Estabilidad de los datos y el modo de omisión
Compose es muy inteligente y puede omitir (skip) la recomposición si sabe que los parámetros de entrada son estables. Un tipo es estable si es inmutable o si el compilador puede garantizar que, si el valor no ha cambiado, la UI no necesita actualizarse. Si usas clases de librerías externas o tipos que Compose considera inestables, la función se recompondrá siempre, aunque los datos sean los mismos.
Para solucionar esto, podemos usar la anotación @Immutable o @Stable en nuestras clases de datos. Si el código está en un módulo externo que no usa el compilador de Compose, podemos recurrir a un archivo de configuración de estabilidad para marcar manualmente esos paquetes como estables. Además, activar el strong-skipping mode en la configuración de Gradle permite que Compose use la igualdad de instancias para omitir funciones incluso con parámetros inestables, optimizando la app sin tocar apenas el código.
Diagnóstico y medición con herramientas reales
No podemos optimizar lo que no podemos medir. El Layout Inspector de Android Studio es la herramienta básica para detectar «puntos calientes» mediante el recuento de recomposiciones. Si ves que un número sube sin parar mientras haces una acción simple, ahí tienes un problema. Para un análisis más profundo, el uso de Macrobenchmark y los registros de Perfetto permite ver exactamente cuánto tiempo consume cada función en el subproceso principal.
Es fundamental evitar que el subproceso principal (Main Thread) se bloquee con tareas pesadas. Cargar imágenes gigantes con painterResource o registrar receptores de sistema puede causar micro-cortes. La solución es mover estas operaciones a corrutinas con Dispatchers.IO y, si es posible, elevar el estado mediante CompositionLocal para que la información esté disponible en todo el árbol sin necesidad de pasarla manualmente por cada parámetro, limpiando así la arquitectura de la interfaz.
Tener una aplicación fluida requiere un equilibrio entre el uso de remember, la correcta gestión de la estabilidad de las clases y la capacidad de posponer la lectura de estados mediante lambdas para aprovechar las fases de diseño y dibujo, apoyándonos siempre en herramientas de medición para validar que cada cambio realmente reduce la carga del sistema. Comparte esta guía y ayuda a otros usuarios a estar informado sobre tema.

