¿Qué hace que un Composable sea 'skippable' y 'stable', y por qué importa para la performance?
No toda recomposición vuelve a ejecutar tu Composable — la estabilidad y la skippability deciden si Compose reutiliza el último frame o paga por reconstruirlo.
APRENDE HACIENDOPon a prueba tu intuición3 pasos
¿Qué hace que un Composable sea 'skippable' y 'stable', y por qué importa para la performance?
Usa los ejemplos y la explicación de la lección para comprobar tu idea.
Reflexión personal. No se envía a ningún servicio de IA.No todo cambio de estado en una app de Compose obliga a que cada composable que teóricamente podría reaccionar termine ejecutándose de nuevo. El runtime de Compose registra qué composables leen qué pedazos de estado, y cuando ese estado cambia sólo invalida a los que lo leyeron — el resto del árbol queda intacto. Pero la invalidación es sólo la mitad de la historia: incluso un composable dentro del scope invalidado a veces puede evitar ejecutar su cuerpo por completo si el compilador puede probar que nada de su salida pudo haber cambiado. Esa evasión es lo que significa "skippable", y es la palanca más grande de performance en Compose que no tiene nada que ver con tu algoritmo y todo que ver con tus tipos.
#Restartable vs. skippable — dos preguntas distintas
El compilador de Compose le hace dos preguntas separadas a cada función @Composable que procesa. Restartable pregunta: ¿puede esta función volver a entrar por sí sola como un scope de recomposición independiente, sin volver a ejecutar a quien la llamó? Casi cualquier composable con al menos un parámetro es restartable. Skippable hace una pregunta más estricta: si a esta función se le pide recomponer, ¿puede el runtime en cambio no hacer nada y reusar lo que ya emitió la última vez? Una función puede ser restartable sin ser skippable — y ese es justo el caso que en silencio le cuesta más performance a los equipos, porque el código se ve bien y nada explota.
#Qué hace que un tipo sea estable
Para poder saltearse una recomposición, todos los parámetros tienen que ser stable: el compilador tiene que poder probar que si dos instancias son equal, nada observable en ellas va a divergir después, y que si una propiedad pública sí cambia, algo (como MutableState) le va a avisar a Compose. Los primitivos, String, y los tipos función (lambdas) son stable por defecto. Una data class armada enteramente de propiedades val de tipos stable es stable. En el momento en que aparece un var, o el tipo de una propiedad es una interfaz que el compilador no puede ver del todo, la estabilidad se rompe — y esto incluye, de forma crítica, a las propias interfaces List, Set y Map de Kotlin, porque en runtime pueden estar respaldadas por un ArrayList o HashMap mutable aunque el tipo declarado no prometa nada de eso. Un parámetro List<Product> es entonces inestable por defecto, y cualquier composable que lo reciba directo pierde la skippability.
// Inestable: una propiedad `var`, más un parámetro con forma de colección de Kotlin.data class ListItemUiModel(var isSelected: Boolean, // var rompe la estabilidadval label: String,)@Composablefun ItemRow(items: List<ListItemUiModel>, onClick: (ListItemUiModel) -> Unit) {// `items` es inestable (la interfaz List), y ListItemUiModel también (var).// Esta función es restartable pero NO skippable.Column {items.forEach { item ->Text(item.label, Modifier.clickable { onClick(item) })}}}
import androidx.compose.runtime.Immutableimport kotlinx.collections.immutable.ImmutableList@Immutabledata class ListItemUiModel(val isSelected: Boolean, // val -> stableval label: String,)@Composablefun ItemRow(items: ImmutableList<ListItemUiModel>, onClick: (ListItemUiModel) -> Unit) {// Ahora tanto la colección como el tipo del elemento son demostrablemente stable —// Compose se saltea esta llamada cuando items y onClick no cambian.Column {items.forEach { item ->Text(item.label, Modifier.clickable { onClick(item) })}}}
@Immutable y @Stable existen para hacerle una promesa al compilador cuando no puede inferir la estabilidad solo — una interfaz que vos controlás, o una clase de una librería que no podés anotar. Mentir con cualquiera de las dos anotaciones es una categoría de bug real: Compose va a saltear recomposiciones basándose en una garantía de estabilidad que la clase después viola, y la UI queda desactualizada en silencio, sin ninguna excepción que señale nada.
#Cómo decide el runtime, paso a paso
#El strong skipping cambia las reglas
El modo strong skipping (disponible desde la versión 1.5.4 del compilador de Compose, y por defecto con el plugin de Compose de Kotlin 2.0) relaja el requisito de estabilidad: un composable puede marcarse como skippable incluso con parámetros inestables, porque el runtime cae de vuelta a comparar la instancia anterior del parámetro por referencia (===) en vez de exigir una prueba de estabilidad. También recuerda automáticamente las lambdas definidas inline dentro de un composable, algo que antes era una tarea manual con remember { }. El strong skipping angosta la brecha entre "escribí Kotlin idiomático" y "escribí Kotlin performante para Compose", pero no hace irrelevante la estabilidad — un tipo genuinamente stable igual se saltea más seguido, porque la igualdad por referencia falla en cada instancia recién construida aunque su contenido sea idéntico.
Esto importa en la práctica en cualquier lado donde la recomposición pasa a escala — un LazyColumn que se reordena en cada tecla de un buscador que no tiene nada que ver, una pantalla de chat que vuelve a correr cada fila de mensaje porque el parámetro de lista es inestable. Saltear una recomposición es casi gratis: un puñado de chequeos de igualdad. Recomponer no lo es: vuelve a correr el cuerpo de la función, vuelve a recorrer la composición, y vuelve a diffear el árbol de UI emitido. La brecha entre esos dos costos, multiplicada a lo largo de una lista con scroll, es donde realmente viven las respuestas a "por qué esta pantalla tranca".
🧠 Check your understanding
0/4 · 0/4 answered1. ¿Por qué un parámetro List<Product> común se trata como inestable en el compilador de Compose?
2. ¿Cuál es la diferencia entre 'restartable' y 'skippable'?
3. ¿Qué cambia el strong skipping mode?
4. ¿Cómo te enterás cuáles de tus composables no son skippable?