Casos de éxito

Cómo los ingenieros de Instagram Direct crearon una arquitectura de IU nativa de IA con Jetpack Compose y redujeron el costo de los 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, que procesa miles de millones de mensajes de usuarios todos los días. Durante años de iteración, el equipo aprovechó cada microoptimización posible del sistema de vistas de Android heredado. 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 de 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 los tokens. En estrecha colaboración con Google, el equipo adoptó Jetpack Compose y, al mismo tiempo, mantuvo un alto nivel de rendimiento. A través de las optimizaciones de rendimiento, Meta y Google mejoraron Compose no solo para Instagram, sino también para el ecosistema de desarrolladores de Android en general.

Modernización de la base de código a gran escala

La IA se convirtió rápidamente en un asistente diario para los ingenieros de la industria, y aplicarla a una base de código a gran escala como la de Instagram ya genera mejoras reales en la productividad. El equipo de Instagram Direct estableció un objetivo más ambicioso. En lugar de simplemente aplicar 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, lo que multiplicó el impacto de la IA mucho más allá de lo que puede ofrecer la adaptación por sí sola.

El equipo de Instagram Direct eligió Jetpack Compose como un componente clave para compilar una arquitectura de IU nativa de 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 estado implícito 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 individuales de la IU 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.

Product Design 1.png

Cuando se migra una base de código de este tamaño a Compose, es tentador tomar el camino fácil e incorporar componentes de la IU de Compose dentro de la jerarquía de vistas 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 crear una arquitectura de IU nativa de IA

A la escala de Instagram, un grado de abstracción arquitectónica es inevitable, y es lo que mantiene la app sostenible a medida que crece. Considera un patrón común, en el que cada tipo de elemento RecyclerView se modela como un descendiente 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 vive como un campo mutable en el elemento en sí, en lugar de en ChatUiState, por lo que sobrevive a la nueva vinculación y al reciclaje de RecyclerView en las filas, lo que genera filtraciones y errores difíciles de reproducir.

Incluso cuando se limpia el código asignándole al elemento una función @Composable dedicada, los mismos 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 sencillo, pero ilustra un problema más amplio: cuantos menos límites se le den a la IA, menor será la calidad del código que produzca con el tiempo. Las barreras de seguridad y las habilidades ayudan, pero no son suficientes por sí solas, ya que, cuando la IA se enfrenta a una fricción, suele buscar una alternativa para desbloquearse.

Para que la base de código sea compatible con la IA, debe seguir dos reglas prácticas: 

  • Minimiza la dependencia del contexto personalizado. Cuanto más conocimiento personalizado y específico de la base de código necesite 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. Corregir 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í debería llevar ese peso. Los agentes de IA toman de forma natural 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, mientras que las decisiones de diseño deficientes deben ser difíciles y costosas de expresar.

Un elemento de lista aún se puede representar 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 hace que sea 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 parte de Direct UI tuvieron que 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 escribir cantidades masivas de código. Ese enfoque es lo que permitió que el equipo de Direct realizara la migración en un 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 tuviera que redescubrirlos. En cada plataforma, el equipo realizó la migración en las siguientes etapas:

  • Escribir todo el código de Compose con IA
  • Pulirla, controlar los casos extremos y cerrar las brechas de rendimiento hasta que la IU se lance a 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, lo que define la arquitectura y los casos extremos difíciles desde el principio. Con esa base, otros pueden enfocarse en preparar la IU para producción sin detenerse a tomar esas decisiones técnicas por sí mismos, lo que mantiene la migración general rápida.

Los resultados de la migración validaron el enfoque. En el caso de las plataformas de Instagram Direct migradas, 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 será el costo de tokens por tarea. 

Quote-Pavlo-New.jpg

En un análisis de datos interno de la base de código de Android para Instagram Direct, se compararon las sesiones de agentes de IA que trabajaban en la IU de Compose con las mismas tareas que usaban vistas de Android. Las mejoras en la eficiencia fueron claras en dos dimensiones:

  • Por carácter de código finalizado: Compose requirió un 32% menos de intercambios entre ingenieros y agentes y un 35% menos de tiempo de ejecución de los agentes (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 general de tokens disminuyó un 33% con Compose en comparación con las vistas.

Informamos tanto la eficiencia de los resultados como las cifras de las sesiones típicas porque son resultados útiles de forma independiente. Las cifras de intercambios entre el ingeniero y el agente, y el tiempo de ejecución 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 hace 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 de producción. En el análisis, se midió la eficiencia de los recursos de un agente con una combinación 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 naturalmente menos eficientes en cuanto a los recursos. 

Cuando se duplica la puntuación de riesgo acumulada de un archivo, la IU implementada con vistas de Android reduce la eficiencia de los recursos de los agentes en un 30% (por carácter que llega). En las mismas circunstancias, la reducción de la IU de Jetpack Compose es de solo un 9%.

A través de la asociación entre Google y Meta, el equipo de Instagram Direct aportó una nueva perspectiva a la adopción de Compose, abordándola 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 solidez de Compose como base para crear bases de código y arquitecturas 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 las personas esperan que sea rápida y responsiva en todo momento. Adoptar Jetpack Compose de manera eficaz 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 vistas 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.  Para la adopción de Compose, los siguientes tres fueron los más importantes:

  • Tiempo de interacción: Es la cantidad de tiempo que transcurre entre que se abre la pantalla y se puede usar.
  • Tiempo de carga completa : Es la cantidad de tiempo que transcurre entre que se abre la pantalla y se carga todo el contenido (p.ej., las imágenes).
  • Rendimiento del desplazamiento: Se refiere a la fluidez con la que se desplaza la pantalla, sin fotogramas perdidos.

Estas métricas se registran durante el tiempo de ejecución en producción, lo que permite realizar pruebas A/B para comparar la IU de Compose migrada con la IU heredada y evaluar el impacto de este esfuerzo en el rendimiento.

Una forma común de abordar una migración como esta es comenzar de a poco, mover un puñado de componentes de la IU, recopilar datos y estudiar cómo se comportan. Si bien son útiles, estos resultados iniciales solo muestran un panorama parcial, ya que proporcionan falsos negativos en relación con la adopción de Compose debido a lo siguiente:

  • No es 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 distinta por motivos que no se generalizan, por lo que no siempre se puede extrapolar a partir de ellos.
  • Costo de interoperabilidad: Una pequeña parte de Compose dentro de una base de código de View grande 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 la migración completa.

Como resultado, las migraciones pequeñas, si bien son útiles, no siempre reflejan el impacto total de Compose. Cuanto más se migre una superficie de extremo a extremo sin interrupciones de puente, más claro y mejor será el panorama en términos de rendimiento.

Las pantallas principales de Instagram Direct se basan en listas largas de varios tipos de elementos, 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 vistas.

Diagram 1.png

La tarea principal del equipo fue una migración gradual de varios cientos de elementos de lista individuales a Compose dentro de la arquitectura existente basada en RecyclerView, que se lanzó en producción en pequeños grupos independientes en pruebas A/B, todo sin cambios visibles en la experiencia de mensajería del usuario.

La mayor desventaja de esta configuración es una dependencia significativa del sistema de View heredado a través de una arquitectura RecyclerView principal, 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 encuentran, sin dejar de ser compatibles con RecyclerView y LazyColumn al mismo tiempo. Igual de importante es la capacidad de cambiar entre las dos opciones en el entorno de ejecución a través de marcas de función para habilitar las pruebas A/B.

Diagram 2.png

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ó implementar la configuración de LazyColumn en una prueba A/B en paralelo con RecyclerView, reutilizando los mismos elementos de Compose y mejorando el rendimiento, sin interrumpir al resto del equipo que creaba y perfeccionaba las 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 abordarlos, se requirió una asociación práctica e iterativa. Los ingenieros de Google y Meta trabajaron en estrecha colaboración para analizar las métricas y, así, identificar y diseñar nuevas funciones de Compose que cumplan con las comparativas basadas en View o las superen. Como resultado de esta asociación, se destacan las siguientes incorporaciones a Jetpack Compose: composición que se puede pausar con LazyLayoutCacheWindows y seguimiento de visibilidad. 

Composición que se puede pausar con LazyLayoutCacheWindows

La composición que se puede pausar (habilitada de forma predeterminada en Compose 1.10) permite que los elementos de listas diferidas costosos se compongan de forma incremental en los fotogramas para evitar bloqueos. Cuando se vincula con LazyLayoutCacheWindow (agregado en Compose 1.9), la combinación mejora significativamente la fluidez del desplazamiento. En pruebas internas recientes en Meta, la combinación de la composición pausable con un LazyLayoutCacheWindow de un viewport redujo las pérdidas de fotogramas grandes por minuto (LFD/m) en aproximadamente un 13% en comparación con Compose estándar. La ventana de caché por sí sola lo redujo en un 8% en comparación con el mismo modelo de referencia. LFDs/m es una métrica interna que Meta usa para hacer un seguimiento de los tirones perceptibles durante el desplazamiento.

Quote-Fabio.jpg


Si usas un LazyLayoutCacheWindow en tu app, se preparan y retienen los elementos fuera de la pantalla dentro de una banda basada en píxeles alrededor del viewport para permitir deslizamientos 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 maneras 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: Es la longitud absoluta fija. ahead = 150.dp mantiene 150 dp de contenido compuesto más allá del borde visible, independientemente del dispositivo.
  • Número de punto flotante: Es una fracción del viewport. aheadFraction = 0.5f mantiene media pantalla compuesta por delante, por lo que la cantidad absoluta se escala con la altura de la pantalla, lo que admite varios factores de forma: más en una tablet o un dispositivo plegable desplegado, y menos en un teléfono compacto. 

El equipo de Instagram ajustó las fracciones flotantes 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. Ofrece a las plataformas de Jetpack Compose a gran escala una forma coherente de saber cuándo un elemento de componibilidad 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 de inicio

La adopción de Jetpack Compose para Instagram Direct generó mejoras de rendimiento inesperadas en otras plataformas de la app. El entorno de ejecución de Jetpack Compose tiene un costo de preparación que pagas solo una vez y, como los mensajes son una plataforma de alto tráfico que se visita con frecuencia al principio de la sesión de un usuario, otras plataformas de Instagram que dependen de Compose mostraron mejoras de rendimiento notables.

El rendimiento de inicio de la IU de Compose dentro de Instagram Direct se optimizó con el uso de perfiles de Baseline, que precompilan instrucciones de código de uso frecuente en el momento de la instalación para que Compose se renderice rápidamente desde el primer inicio.

Lessons from the Instagram Direct migration to Jetpack Compose 

  • Jetpack Compose tiene un retorno de la inversión inmediato: No necesitas usar flujos de trabajo de IA avanzados para beneficiarte de Compose. Con la reducción de aproximadamente un 50% en el código, hay menos código que mantener y se reduce el área de superficie para los errores.
  • El diseño de una arquitectura nativa de IA generó logros significativos, como una reducción del 35% en el tiempo de ejecución de los agentes de IA, un 32% menos de intercambios entre ingenieros y agentes, y una reducción del 33% en el costo de los 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.
  • Vincula la composición de Pausable con LazyLayoutCacheWindow: Vincular estos dos elementos produce mejores resultados que las ventanas de caché por sí solas. Solo con la ventana de caché, un elemento pesado 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 y sus ideas en 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. Por lo tanto, comparte tus comentarios.

La adopción de Jetpack Compose generó mejoras 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 por igual. 

Si aún no probaste Compose, ahora con asistencia de IA, migrar a Jetpack Compose es más fácil que nunca.

Agradecimientos. 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.

Escrito por:
Continuar leyendo