Cómo los ingenieros de Instagram Direct crearon una arquitectura de IU nativa para la IA con Jetpack Compose y redujeron el costo de tokens por sesión de agente en un 33%
Lectura de 11 min
Esta entrada de blog se escribió en colaboración con el equipo de Meta
Instagram Direct es una de las plataformas principales de Instagram, ya que maneja miles de millones de mensajes de usuarios todos los días. A lo largo de los años de iteración, el equipo aprovechó al máximo cada microoptimización posible del sistema heredado de Android View. Sin embargo, mantener y expandir una plataforma heredada muy optimizada genera una deuda técnica y una sobrecarga de ingeniería significativas, en especial a medida que los equipos adoptan cada vez más la IU declarativa y los asistentes de programación basados en IA.
La adopción de Jetpack Compose para Instagram Direct fue más allá de una modernización típica de la IU. El equipo creó una base de código de IU nativa de IA que es un 50% más pequeña que la implementación original, a la vez que logró una reducción del 35% en el tiempo de ejecución del agente de IA, un 32% menos de intercambios entre ingenieros y agentes y una reducción del 33% en el costo de tokens. En estrecha colaboración con Google, el equipo adoptó Jetpack Compose y mantuvo un alto nivel de rendimiento. Gracias a las optimizaciones del rendimiento, Meta y Google mejoraron Compose no solo para Instagram, sino también para el ecosistema más amplio de desarrolladores de Android.
Modernización de la base de código a gran escala
La IA se convirtió rápidamente en un compañero diario para los ingenieros de la industria, y su aplicación a una base de código a gran escala como la de Instagram ya genera ganancias reales en la productividad. El equipo de Instagram Direct estableció un objetivo más ambicioso. En lugar de simplemente dirigir las herramientas de IA al código existente, el equipo rediseñó la base de código y su arquitectura para que fueran nativas de la IA por diseño, lo que multiplicó el impacto de la IA mucho más allá de lo que la adaptación por sí sola puede ofrecer.
El equipo de Instagram Direct eligió Jetpack Compose como componente clave para compilar una arquitectura de IU nativa de la IA. Su naturaleza declarativa garantiza que el código sea conciso, predecible y estructuralmente más fácil de razonar para los modelos de IA, con menos efectos secundarios, menos estados implícitos y límites de componentes más claros.
La migración a Jetpack Compose requirió una planificación cuidadosa. Cientos de millones de personas envían mensajes en Instagram todos los días, por lo que la migración debía ser gradual y fluida, sin interrupciones en la experiencia mientras el equipo rediseñaba la base subyacente. Para ilustrar la magnitud del desafío, los componentes de la IU individuales se pueden renderizar en más de 160 permutaciones de estado distintas, y una sola pantalla de conversación controla más de 200 tipos de mensajes distintos.
Cuando se migra una base de código de este tamaño a Compose, es tentador tomar el camino fácil y, luego, incorporar componentes de la IU de Compose dentro de la jerarquía de View existente. Como paso incremental durante una migración gradual, es perfectamente válido. Sin embargo, a largo plazo, integrar Compose en una base de código basada en View plantea un desafío. Las herramientas de IA suelen tomar el camino más fácil. Si mezclas código de IU declarativo e imperativo, es probable que la IA los combine de forma incorrecta, lo que generará errores sutiles, deuda técnica y regresiones de rendimiento.
Cómo compilar una arquitectura de IU nativa de la IA
En la escala de Instagram, un cierto grado de abstracción arquitectónica es inevitable, y es lo que mantiene la aplicación en condiciones de mantenimiento a medida que crece. Considera un patrón común, en el que cada tipo de elemento RecyclerView se modela como un elemento secundario de una clase base RecyclerViewItem personalizada que expone hooks de ciclo de vida habituales, como onBind.
Ejemplo 1
class ChatItem( val features: FeatureFlagProvider ) : RecyclerViewItem<ComposeViewHolder, ChatUiState> { // Imperative context: // AI could often take the path of least resistance and generate a mutable // state here, dispatched outside the ChatUiState. This class survives // re-bindings and is shared across multiple items, ultimately leading to // unexpected, hard-to-reproduce bugs. var isPinned: Boolean = false override fun onBind(holder: ComposeViewHolder, uiState: ChatUiState) { // Imperative context val isPinnedChatsEnabled = features.isEnabled("pinned_chats_feature") // Declarative context holder.composeView.setContent { // Blending imperative and declarative contexts if (isPinnedChatsEnabled) { Button(onClick = { isPinned = !isPinned }) { Text(if (isPinned) "Unpin" else "Pin") } } ... } } }
En el fragmento anterior, se presentan dos problemas. Primero, la marca isPinnedChatsEnabled se lee en el código imperativo y, luego, se captura dentro de una lambda de Compose, un acoplamiento sutil entre paradigmas. En segundo lugar, isPinned existe como un campo mutable en el elemento en sí, en lugar de en ChatUiState, por lo que sobrevive a la vinculación y el reciclaje de RecyclerView en las filas, lo que genera fugas y errores difíciles de reproducir.
Incluso cuando se limpia el código asignándole al elemento una función @Composable dedicada, los problemas persisten.
Ejemplo 2
class ChatItem( val features: FeatureFlagProvider ) : ComposeRecyclerViewItem<ChatUiState> { // Imperative context val isPinnedChatsEnabled = features.isEnabled("pinned_chats_feature") var isPinned: Boolean = false // Declarative context @Composable override fun Content(uiState: ChatUiState) { // Blending imperative and declarative contexts if (isPinnedChatsEnabled) { Button(onClick = { isPinned = !isPinned }) { Text(if (isPinned) "Unpin" else "Pin") } } ... } }
Este es un ejemplo simple a propósito, pero ilustra un problema más amplio: Cuantas menos limitaciones se le impongan a la IA, menor será la calidad del código que produzca con el tiempo. Las protecciones y las habilidades ayudan, pero no son suficientes por sí solas, ya que, cuando la IA encuentra obstáculos, a menudo los evita para desbloquearse.
Para que la base de código sea apta para la IA, debe seguir dos reglas prácticas:
- Minimiza la dependencia del contexto personalizado. Cuanto más específico sea el conocimiento de la base de código que necesita un agente de IA para realizar un cambio correcto, menor será la calidad de su resultado. Cuanto más se acerque la base de código a las prácticas recomendadas conocidas, mejores serán los resultados de la IA.
- Una base de código centrada en la IA debe aplicar sus propios límites. Parchear las brechas de diseño con habilidades de IA no es escalable, ya que cada habilidad cargada en el contexto cuesta tokens y puede degradar el rendimiento del agente. En cambio, la arquitectura en sí debe tener ese peso. Los agentes de IA naturalmente toman el camino de menor resistencia, por lo que el diseño debe hacer que ese camino conduzca a un código correcto y de alta calidad, al mismo tiempo que dificulte y encarezca expresar decisiones de diseño deficientes.
Un elemento de lista aún puede representarse con su propia abstracción, pero, en este caso, todo el código de Compose se encuentra en el constructor, por lo que no tiene acceso a los miembros ni al estado de la clase, y su única fuente de argumentos es el constructor. Esto la hace equivalente a una función @Composable simple, a la vez que se ajusta a la arquitectura existente.
Ejemplo 3
class ChatItem( val features: FeatureFlagProvider, val onPin: (Boolean) -> Unit, ) : ComposeItem<ChatUiState>( // Compose UI content = { uiState: ChatUiState -> val isPinnedChatsEnabled = features.isEnabled("pinned_chats_feature") if (isPinnedChatsEnabled) { Button(onClick = { onPin(!uiState.isPinned) }) { Text(if (uiState.isPinned) "Unpin" else "Pin") } } ... }, )
Migrar una base de código de este tamaño es una tarea enorme. Durante mucho tiempo, los cientos de componentes de la IU que conforman la mayoría de la IU directa debieron coexistir con sus contrapartes heredadas, y ambos se mantuvieron en paralelo. Los flujos de trabajo de IA ayudaron a que esta migración paralela fuera posible, ya que aceleraron el proceso de escritura de grandes cantidades de código. Ese enfoque permitió que el equipo de Direct realizara la migración en tiempo récord, sin interrumpir al resto del equipo, que siguió lanzando las funciones que mejoran la experiencia de millones de personas todos los días.
Varios ingenieros ejecutaron sus propios agentes de IA en una base de conocimiento compartida de habilidades y convenciones reutilizables creadas durante la migración. Esto mantuvo los flujos de trabajo y las prácticas recomendadas sincronizados en todo el equipo, en lugar de que cada ingeniero los redescubriera. En cada plataforma, el equipo realizó la migración en las siguientes etapas:
- Escribir todo el código de Compose con IA
- La pulimos, abordamos los casos extremos y cerramos las brechas de rendimiento hasta que se lanzó la IU para los usuarios reales en una prueba pública.
Dividir el trabajo en dos etapas por pantalla permite que un ingeniero avance rápidamente por toda la superficie, estableciendo la arquitectura y los casos extremos difíciles desde el principio. Con esa base establecida, otros pueden enfocarse en preparar la IU para la producción sin detenerse a tomar esas decisiones técnicas por su cuenta, lo que mantiene la migración general rápida.
Los resultados de la migración validaron el enfoque. En el caso de las superficies migradas de Instagram Direct, Jetpack Compose permitió que el equipo redujera la cantidad total de código de la IU en un 50%. Cuanto menos código deba generar la IA, mayor será la calidad del resultado y menor el costo de tokens por tarea.
Un análisis interno de datos de la base de código de Android para Instagram Direct comparó las sesiones de agentes de IA que trabajaban en la IU de Compose con las mismas tareas que usaban Android Views. Las mejoras en la eficiencia fueron evidentes en dos dimensiones:
- Por carácter de código completado: Compose requirió un 32% menos de intercambios entre el ingeniero y el agente y un 35% menos de tiempo de ejecución del agente (el tiempo transcurrido desde que un agente comienza a trabajar en la solicitud de un ingeniero hasta que devuelve una respuesta).
- Por sesión de agente: El costo de tokens general disminuyó un 33% con Compose en comparación con Views.
Informamos la eficiencia de salida y las cifras típicas de sesiones porque son resultados útiles de forma independiente. Las cifras de intercambio y tiempo de ejecución del agente de ingeniería comparan el uso de recursos por unidad de resultado final, mientras que la cifra de tokens compara el costo total de una sesión típica del agente.
Los datos también revelaron una diferencia constante en la forma en que los dos frameworks manejan el código complejo o frágil. Meta realiza un seguimiento de esto con una puntuación de riesgo de los cambios de código, que evalúa la calidad general del código y la probabilidad de que un cambio cause incidentes en la producción. El análisis midió la eficiencia de recursos de un agente con un compuesto del consumo de tokens, el tiempo de ejecución del agente y las interacciones entre el ingeniero y el agente. A medida que los archivos acumulan una puntuación de riesgo más alta, las sesiones de los agentes de IA se vuelven menos eficientes en el uso de recursos.
Cuando la puntuación de riesgo acumulada de un archivo se duplica, la IU implementada con Android Views reduce la eficiencia de los recursos del agente en un 30% (por carácter aterrizado). En las mismas circunstancias, la reducción de la IU de Jetpack Compose es solo del 9%.
Gracias a la asociación entre Google y Meta, el equipo de Instagram Direct aportó una nueva perspectiva a la adopción de Compose, ya que la abordó desde el punto de vista de la preparación de la base de código para la IA, no solo como una reescritura de la IU. Este trabajo reveló la fortaleza de Compose como base para crear arquitecturas y bases de código centradas en la IA, especialmente cuando se aplica a la escala de apps como Instagram.
Optimizaciones del rendimiento
Instagram Direct es una de las plataformas más integrales de la app, y los usuarios esperan que se sienta rápido y responsivo en todo momento. Adoptar Jetpack Compose de manera efectiva implicó una reescritura sustancial de la IU, y el objetivo principal era preservar la experiencia de alta calidad sin regresiones.
Años de iteración ya habían llevado la implementación heredada basada en View en Instagram a un nivel de rendimiento excepcionalmente alto, y el equipo necesitaba cumplir con ese mismo estándar mientras se cambiaba a un framework de IU completamente nuevo.
Instagram mide cientos, si no miles, de métricas de rendimiento. En cuanto a la adopción de Compose, los siguientes tres fueron los más importantes:
- Tiempo de interacción: Es el tiempo que transcurre entre la apertura de la pantalla y el momento en que se puede usar.
- Tiempo de carga completa: Es el tiempo que transcurre desde que se abre la pantalla hasta que se carga todo el contenido (es decir, las imágenes).
- Rendimiento de desplazamiento: Indica qué tan fluido es el desplazamiento de la pantalla, sin fotogramas perdidos.
Estas métricas se registran durante el tiempo de ejecución en producción, lo que permite ejecutar pruebas A/B para comparar la IU de Compose migrada con la IU heredada y evaluar el impacto en el rendimiento de este esfuerzo.
Una forma común de abordar una migración como esta es comenzar con una pequeña cantidad de componentes de la IU, recopilar datos y estudiar su comportamiento. Si bien son útiles, estos primeros resultados solo brindan una imagen parcial, lo que genera falsos negativos en relación con la adopción de Compose por los siguientes motivos:
- No representativo: Un componente de IU migrado puede proporcionar datos útiles sobre su rendimiento general en una pantalla en particular. Sin embargo, los diferentes componentes se comportan de manera diferente por motivos que no se generalizan, por lo que no siempre puedes extrapolar a partir de él.
- Costo de interoperabilidad: Una pequeña parte de Compose dentro de una gran base de código de View paga un costo de puente impredecible entre los dos sistemas. Esa sobrecarga distorsiona la medición, por lo que los primeros resultados a pequeña escala no reflejan cómo sería realmente la migración completa.
El resultado es que las migraciones pequeñas, si bien son útiles, no siempre reflejan el impacto completo de Compose. Cuanto más se migra una plataforma de extremo a extremo sin interrupciones de puente, más clara y mejor se vuelve la imagen en términos de rendimiento.
Las pantallas principales de Instagram Direct se basan en listas largas de tipos de elementos variados, que originalmente se implementaron con RecyclerView. La arquitectura se basa en abstracciones personalizadas para la escalabilidad, pero sigue vinculada al ciclo de vida del sistema basado en View.
La tarea principal del equipo fue migrar gradualmente varios cientos de elementos de lista individuales a Compose dentro de la arquitectura existente basada en RecyclerView, y lanzarlos en producción en pequeños grupos independientes bajo pruebas A/B, todo sin cambios visibles en la experiencia de mensajería del usuario.
El mayor inconveniente de esta configuración es una dependencia significativa del sistema View heredado a través de una arquitectura RecyclerView central, incluso después de la migración completa de cada elemento de la lista a Compose. Como siguiente paso natural, el equipo decidió invertir en reemplazar la arquitectura principal basada en RecyclerView por la alternativa nativa de Compose: LazyColumn.
Esto significa que los componentes de la IU de Compose deben abstraerse del framework en el que se incluyen y, al mismo tiempo, ser compatibles con RecyclerView y LazyColumn. Igualmente importante es la capacidad de cambiar entre los dos en el tiempo de ejecución a través de marcas de funciones para habilitar las pruebas A/B.
Si bien los nuevos elementos de Compose son compatibles de forma nativa con LazyColumn y se pueden conectar a un árbol de composición ininterrumpido, también se creó una API de interoperabilidad para insertarlos en un RecyclerView. Esto permitió lanzar la configuración de LazyColumn en una prueba A/B junto con RecyclerView, reutilizando los mismos elementos de Compose y mejorando el rendimiento, sin interrumpir al resto del equipo que estaba creando y perfeccionando funciones.
La escala, la complejidad y la sensibilidad de Instagram incluso a las regresiones más pequeñas representaron un desafío único para Jetpack Compose. Para abordar estos problemas, se requirió una asociación iterativa y práctica. Los ingenieros de Google y Meta trabajaron en estrecha colaboración y analizaron las métricas para identificar y diseñar nuevas capacidades de Compose que cumplieran o superaran los parámetros de referencia basados en View. Como resultado de esta asociación, se destacan las siguientes incorporaciones a Jetpack Compose: composición pausable con LazyLayoutCacheWindows y seguimiento de visibilidad.
Composición pausable con LazyLayoutCacheWindows
La composición pausable (habilitada de forma predeterminada en Compose 1.10) permite que los elementos costosos de la lista diferida se compongan de forma incremental en los fotogramas para evitar la latencia. Cuando se combina con LazyLayoutCacheWindow (agregado en Compose 1.9), la combinación mejora significativamente la fluidez del desplazamiento. En las pruebas internas recientes de Meta, la combinación de la composición pausable con un LazyLayoutCacheWindow de una sola ventana redujo las grandes pérdidas de fotogramas por minuto (LFD/m) en aproximadamente un 13% en comparación con Compose estándar. Cache Window por sí sola lo redujo en aproximadamente un 8% en comparación con el mismo valor de referencia. Los LFD/m son una métrica interna que Meta usa para hacer un seguimiento de las interrupciones notables durante el desplazamiento.
Usar un LazyLayoutCacheWindow en tu app prepara y retiene elementos fuera de la pantalla dentro de una banda basada en píxeles alrededor del viewport para habilitar movimientos rápidos. Para aprovechar LazyLayoutCacheWindows en tu app, puedes usar la versión más reciente de Compose 1.13.0-alpha03 y configurarla como se muestra en el siguiente ejemplo:
val cacheWindow = LazyLayoutCacheWindow(ahead = 150.dp, behind = 100.dp) // OR val cacheWindow = LazyLayoutCacheWindow(aheadFraction = 0.5f, behindFraction = 0.3f) LazyColumn(state = state, cacheWindow = cacheWindow) { ... }
Existen dos formas de configurar la ventana de caché. Ambos describen lo mismo: cuánto contenido fuera de la pantalla se debe mantener compuesto, pero en diferentes unidades.
- Dp: Longitud absoluta fija.
ahead = 150.dpmantiene 150 dp de contenido compuesto más allá del borde visible, independientemente del dispositivo. - Float: Fracción del viewport.
aheadFraction = 0.5fmantiene la mitad de una pantalla compuesta con anticipación, por lo que la cantidad absoluta se ajusta con la altura de la pantalla y admite varios factores de forma: más en una tablet o un dispositivo plegable desplegado, menos en un teléfono compacto.
El equipo de Instagram ajustó las fracciones de flotación de la ventana de caché específicamente para la estructura de contenido y los tamaños de los elementos de Direct. Dado que los valores ideales varían según los parámetros específicos de la IU, encontrar el equilibrio adecuado requiere cierta experimentación.
Registro de impresiones con onVisibilityChanged
La API de onVisibilityChanged (agregada en Compose 1.9.0) fue otro resultado clave de la asociación técnica entre Google y Meta. Brinda a las superficies de Jetpack Compose a gran escala una forma coherente de saber cuándo un elemento componible es realmente visible en la pantalla, lo que reemplaza las implementaciones personalizadas y manuales que se usaban en el pasado. Solo en Instagram Direct, estos indicadores de visibilidad se usan en cientos de archivos para respaldar las métricas de calidad del producto que dependen de si los elementos de la IU se mostraron realmente a las personas.
Rendimiento del inicio
La adopción de Jetpack Compose para Instagram Direct generó mejoras inesperadas en el rendimiento en otras plataformas de la app. El tiempo de ejecución de Jetpack Compose tiene un costo de preparación que se paga solo una vez y, dado que la mensajería es una plataforma de alto tráfico que se visita a menudo al principio de una sesión del usuario, otras plataformas de Instagram que dependen de Compose experimentaron mejoras notables en el rendimiento.
El rendimiento de inicio de la IU de Compose dentro de Instagram Direct se optimizó con perfiles de Baseline, que precompilan instrucciones de código activas en el momento de la instalación para que Compose renderice rápidamente desde el primer inicio.
Lecciones de la migración de Instagram Direct a Jetpack Compose
- Jetpack Compose ofrece un retorno de la inversión inmediato: No es necesario que uses flujos de trabajo avanzados de IA para beneficiarte de Compose. Con la reducción del código en un 50%, se requiere menos código para mantener y se reduce la superficie de errores.
- El diseño de una arquitectura nativa para la IA generó logros significativos, como una reducción del 35% en el tiempo de ejecución del agente de IA, un 32% menos de intercambios entre ingenieros y agentes, y una reducción del 33% en el costo de tokens.
- Si bien hay muchas APIs de interoperabilidad y compatibilidad para combinar Views y Compose, intenta migrar superficies más grandes en lugar de componentes pequeños individuales. Esto mantiene la IU dentro de una sola jerarquía de composición ininterrumpida y desbloquea todas las mejores optimizaciones de rendimiento nativas de Compose.
- Combina Pausable composition con LazyLayoutCacheWindow: La combinación de estos dos elementos produce mejores resultados que las ventanas de caché por sí solas. Con solo la ventana de caché, un elemento pesado aún podría intentar componerse en una sola pasada, lo que podría exceder el presupuesto de fotogramas.
- Contribuye a Compose Meta se asoció con el equipo de Jetpack Compose para dar vida a sus comentarios e ideas dentro de Compose. Trabajar en un kit de herramientas de código abierto significa que todos nos beneficiamos cuando se realizan correcciones de errores y mejoras de rendimiento de forma centralizada. Así que comparte tus comentarios.
La adopción de Jetpack Compose generó ganancias significativas en el desarrollo asistido por IA y, al mismo tiempo, simplificó la ingeniería de IU diaria en Instagram. El enfoque declarativo reduce el código estándar, facilita la comprensión del estado y mejora la productividad general de los desarrolladores. El equipo de ingeniería de Instagram espera llevar Compose a más plataformas de la app y continuar la colaboración entre Google y Meta para brindar más mejoras a los usuarios de Instagram y Jetpack Compose.
Si aún no probaste Compose, ahora con asistencia de IA, migrar a Jetpack Compose es más fácil que nunca.
Reconocimientos. Agradecemos a Michal Zielinski y Matthew Du de Meta, y a Andrei Shikov y George Mount de Google por su trabajo para mejorar el rendimiento de Compose a través de la colaboración entre Meta y Google. También agradecemos a Gary Ye de Meta por ayudar a llevar Compose a Instagram Direct y a Gopal Juneja de Meta por apoyar este esfuerzo a través de la ciencia de datos.
-
Casos de éxitoWhatsApp es la plataforma de mensajería más grande del mundo y presta servicios a miles de millones de usuarios en todo el mundo. Es la herramienta de comunicación predeterminada para las personas de diversas regiones, ya que conecta a los usuarios a través de mensajes privados, confiables y seguros.
Niharika Arora, Tracy Agyemang, Mayank Jain • Lectura de 8 min -
Casos de éxitoTinder reduce los inicios en frío de la app en un 47% con el nuevo Analizador de configuración de R8
La misión de Tinder es potenciar e inspirar conexiones reales haciendo que conocer gente sea fácil y divertido para cada nueva generación de solteros.
Ajesh Pai, Ulises Uriel Verduzco Díaz , Tracy Agyemang • Lectura de 4 min -
Casos de éxitoDado que la mayoría de las apps para Android adoptaron Kotlin como su lenguaje principal, kotlinx.coroutines se convirtió en un estándar de facto para la programación asíncrona. La biblioteca ofrece una forma bien diseñada y estructurada de administrar flujos simultáneos que es nativa de Kotlin.
Jonathan Starup, Andrei Shikov • Lectura de 7 min
Recibe la información más reciente sobre el desarrollo de Android en tu bandeja de entrada todas las semanas.