Comment les ingénieurs d'Instagram Direct ont créé une architecture d'UI native de l'IA avec Jetpack Compose et réduit le coût des jetons par session d'agent de 33%
Temps de lecture : 11 min
Cet article de blog a été rédigé en collaboration avec l'équipe Meta.
Instagram Direct est l'une des principales surfaces d'Instagram, qui traite des milliards de messages utilisateur chaque jour. Au fil des années, l'équipe a effectué toutes les micro-optimisations possibles sur l'ancien système Android View. Toutefois, la maintenance et l'expansion d'une ancienne surface fortement optimisée entraînent une dette technique et une surcharge d'ingénierie importantes, en particulier à mesure que les équipes adoptent de plus en plus les assistants de codage d'IA et les UI déclaratives.
L'adoption de Jetpack Compose pour Instagram Direct est allée au-delà d'une simple modernisation de l'UI. L'équipe a créé une base de code d'UI native de l'IA qui est 50% plus petite que l'implémentation d'origine, tout en réduisant de 35% le temps d'exécution de l'agent d'IA, de 32% le nombre d'échanges entre l'ingénieur et l'agent et de 33% le coût en jetons. En étroite collaboration avec Google, l'équipe a adopté Jetpack Compose tout en maintenant un niveau de performances élevé. Grâce aux optimisations des performances, Meta et Google ont amélioré Compose non seulement pour Instagram, mais aussi pour l'écosystème des développeurs Android au sens large.
Moderniser le codebase à grande échelle
L'IA est rapidement devenue un compagnon quotidien pour les ingénieurs du secteur, et son application à une base de code à grande échelle comme Instagram génère déjà de réels gains de productivité. L'équipe Instagram Direct s'est fixé un objectif plus ambitieux. Au lieu de simplement pointer les outils d'IA vers le code existant, l'équipe a repensé la base de code et son architecture pour qu'elles soient natives de l'IA. L'impact de l'IA a ainsi été multiplié par rapport à ce qu'une simple adaptation aurait pu apporter.
L'équipe Instagram Direct a choisi Jetpack Compose comme composant clé pour créer une architecture d'UI native à l'IA. Sa nature déclarative garantit que le code est concis, prévisible et structurellement plus facile à raisonner pour les modèles d'IA, avec moins d'effets secondaires, moins d'état implicite et des limites de composants plus claires.
La migration vers Jetpack Compose a nécessité une planification minutieuse. Des centaines de millions de personnes envoient des messages sur Instagram chaque jour. La migration devait donc être progressive et fluide, sans perturber l'expérience utilisateur pendant que l'équipe réarchitecturait la base de données. Pour illustrer l'ampleur du défi : les composants d'UI individuels peuvent s'afficher dans plus de 160 permutations d'état distinctes, et un seul écran de conversation gère à lui seul plus de 200 types de messages distincts.
Lorsque vous migrez une codebase de cette taille vers Compose, il est tentant de choisir la solution de facilité et d'intégrer les composants de l'UI Compose dans la hiérarchie de vues existante. C'est tout à fait acceptable si vous effectuez une migration progressive. Toutefois, l'intégration de Compose dans une codebase basée sur les vues pose un problème à long terme. Les outils d'IA empruntent souvent le chemin le plus simple. Si vous mélangez du code d'UI déclaratif et impératif, l'IA risque de les mélanger de manière incorrecte, ce qui peut entraîner des bugs subtils, une dette technique et des régressions de performances.
Créer une architecture d'UI native à l'IA
À l'échelle d'Instagram, un certain degré d'abstraction architecturale est inévitable. C'est ce qui permet de maintenir l'application à mesure qu'elle se développe. Prenons un schéma courant, où chaque type d'article RecyclerView est modélisé comme descendant d'une classe de base RecyclerViewItem personnalisée qui expose les hooks de cycle de vie habituels tels que onBind.
Exemple 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") } } ... } } }
Dans l'extrait ci-dessus, deux problèmes se posent. Tout d'abord, l'indicateur isPinnedChatsEnabled est lu dans le code impératif, puis capturé dans un lambda Compose, un couplage subtil entre les paradigmes. Deuxièmement, isPinned est un champ mutable sur l'élément lui-même plutôt que dans ChatUiState. Il survit donc à la liaison et au recyclage RecyclerView sur plusieurs lignes, ce qui entraîne des fuites et des bugs difficiles à reproduire.
Même lorsque le code est nettoyé en attribuant à l'élément une fonction @Composable dédiée, les mêmes problèmes persistent.
Exemple 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") } } ... } }
Il s'agit volontairement d'un exemple simple, mais il illustre un problème plus général : moins l'IA reçoit de limites, plus la qualité du code qu'elle produit diminue au fil du temps. Les garde-fous et les compétences sont utiles, mais ne suffisent pas à eux seuls, car lorsque l'IA rencontre des difficultés, elle les contourne souvent pour se débloquer.
Pour que la codebase soit compatible avec l'IA, elle doit respecter deux règles pratiques :
- Réduisez la dépendance au contexte personnalisé. Plus un agent d'IA a besoin de connaissances spécifiques à un codebase pour effectuer une modification correcte, moins la qualité de son résultat est élevée. Plus la codebase est proche des bonnes pratiques connues, meilleurs sont les résultats de l'IA.
- Un codebase axé sur l'IA doit faire respecter ses propres limites. Combler les lacunes de conception avec des compétences en IA n'est pas évolutif, car chaque compétence chargée dans le contexte coûte des jetons et peut dégrader les performances de l'agent. C'est l'architecture elle-même qui doit porter ce poids. Les agents d'IA empruntent naturellement le chemin de moindre résistance. La conception doit donc faire en sorte que ce chemin mène à un code correct et de haute qualité, tout en rendant les mauvaises décisions de conception difficiles et coûteuses à exprimer.
Un élément de liste peut toujours être représenté par sa propre abstraction, mais dans ce cas, tout le code Compose se trouve dans le constructeur. Il n'a donc pas accès aux membres ni à l'état de la classe, et sa seule source d'arguments est le constructeur. Elle est ainsi équivalente à une fonction @Composable simple, tout en respectant l'architecture existante.
Exemple 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") } } ... }, )
Migrer un codebase de cette taille est une tâche colossale. Pendant longtemps, les centaines de composants d'UI qui constituent la majorité de l'UI directe ont dû coexister avec leurs homologues hérités, les deux étant gérés en parallèle. Les workflows d'IA ont permis cette migration parallèle en accélérant le processus d'écriture de grandes quantités de code. Cette approche a permis à l'équipe Direct d'effectuer la migration en un temps record, sans perturber le reste de l'équipe, qui a continué à déployer les fonctionnalités qui améliorent l'expérience de millions de personnes chaque jour.
Plusieurs ingénieurs ont exécuté leurs propres agents d'IA sur une base de connaissances partagée de compétences et de conventions réutilisables, créée lors de la migration. Cela a permis de synchroniser les workflows et les bonnes pratiques pour toute l'équipe, au lieu de laisser chaque ingénieur les redécouvrir. Pour chaque surface, l'équipe a effectué la migration en plusieurs étapes :
- Écrivez tout le code Compose avec l'IA.
- Nous l'avons peaufinée, en gérant les cas extrêmes et en comblant les lacunes en termes de performances, jusqu'à ce que l'UI soit déployée auprès de vrais utilisateurs lors d'un test public.
En divisant le travail en deux étapes par écran, un ingénieur peut parcourir rapidement toute la surface, en réglant l'architecture et les cas extrêmes délicats en amont. Une fois ces bases établies, les autres membres de l'équipe peuvent se concentrer sur la préparation de l'UI pour la production sans avoir à prendre eux-mêmes ces décisions techniques, ce qui permet d'accélérer la migration globale.
Les résultats de la migration ont validé l'approche. Pour les surfaces Instagram Direct migrées, Jetpack Compose a permis à l'équipe de réduire de 50%la quantité totale de code d'UI. Moins l'IA a de code à générer, plus la qualité du résultat est élevée et moins le coût en jetons par tâche est important.
Une analyse de données interne de la codebase Android pour Instagram Direct a comparé les sessions d'agents IA travaillant sur Compose UI aux mêmes tâches utilisant Android Views. Les gains d'efficacité étaient évidents dans deux domaines :
- Par caractère de code final : Compose nécessite 32% d'échanges ingénieur-agent en moins et 35% de temps d'exécution de l'agent en moins(temps écoulé entre le moment où un agent commence à travailler sur la demande d'un ingénieur et le moment où il renvoie une réponse).
- Par session d'agent : le coût global en jetons a diminué de 33% avec Compose par rapport aux vues.
Nous indiquons à la fois l'efficacité de la production et le nombre de sessions habituel, car ce sont des résultats utiles indépendamment les uns des autres. Les chiffres sur les échanges et le temps d'exécution de l'agent d'ingénierie comparent l'utilisation des ressources par unité de résultat obtenu, tandis que le chiffre des jetons compare le coût total d'une session d'agent typique.
Les données ont également révélé une différence constante dans la façon dont les deux frameworks gèrent le code complexe ou fragile. Meta suit cette métrique à l'aide d'un score de risque des modifications du code, qui évalue la qualité globale du code et la probabilité qu'une modification entraîne des incidents de production. L'analyse a mesuré l'efficacité des ressources d'un agent à l'aide d'un composite de la consommation de jetons, du temps d'exécution de l'agent et des interactions entre l'ingénieur et l'agent. À mesure que le score de risque des fichiers augmente, les sessions d'agents IA deviennent naturellement moins économes en ressources.
Lorsque le score de risque cumulé d'un fichier double, l'UI implémentée avec Android Views réduit l'efficacité des ressources de l'agent de 30% (par caractère déposé). Dans les mêmes circonstances, la réduction par l' UI Jetpack Compose n'est que de 9%.
Grâce à un partenariat entre Google et Meta, l'équipe Instagram Direct a apporté une nouvelle perspective à l'adoption de Compose. Elle l'a abordée sous l'angle de l'aptitude du code à l'IA, et pas seulement d'une réécriture de l'UI. Ce travail a révélé la force de Compose en tant que base pour la création de bases de code et d'architectures axées sur l'IA, en particulier lorsqu'il est appliqué à l'échelle d'applications comme Instagram.
Optimisations des performances
Instagram Direct est l'une des surfaces les plus importantes de l'application. Les utilisateurs s'attendent à ce qu'elle soit rapide et réactive à tout moment. L'adoption de Jetpack Compose impliquait une réécriture substantielle de l'UI. L'objectif premier était de préserver une expérience de haute qualité sans régression.
Des années d'itération avaient déjà permis à l'ancienne implémentation basée sur les vues sur Instagram d'atteindre un niveau de performances exceptionnellement élevé. L'équipe devait respecter cette même norme tout en passant à un tout nouveau framework d'UI.
Instagram mesure des centaines, voire des milliers, de métriques de performances. Pour l'adoption de Compose, les trois suivants étaient les plus importants :
- Temps d'interaction : durée entre l'ouverture de l'écran et la possibilité de l'utiliser.
- Temps de chargement complet : durée entre l'ouverture de l'écran et le chargement complet de tout le contenu (images, par exemple).
- Performances de défilement : fluidité du défilement de l'écran, sans perte d'images.
Ces métriques sont suivies au moment de l'exécution en production, ce qui permet d'effectuer des tests A/B comparant l'UI Compose migrée à l'ancienne UI et d'évaluer l'impact de cet effort sur les performances.
Une approche courante pour une migration comme celle-ci consiste à commencer petit, en transférant une poignée de composants d'interface utilisateur, en collectant des données et en étudiant leur comportement. Bien qu'utiles, ces premiers résultats ne donnent qu'une image partielle, fournissant de faux négatifs par rapport à l'adoption de Compose, car :
- Non représentatif : un composant d'UI migré peut fournir des données utiles sur ses performances globales sur un écran particulier. Toutefois, différents composants se comportent différemment pour des raisons qui ne sont pas généralisables. Vous ne pouvez donc pas toujours extrapoler à partir de cela.
- Coût d'interopérabilité : un petit élément Compose dans une grande base de code View entraîne un coût de pontage imprévisible entre les deux systèmes. Cette surcharge fausse la mesure. Les premiers résultats à petite échelle ne reflètent donc pas ce à quoi ressemblerait réellement une migration complète.
Par conséquent, les petites migrations, bien qu'utiles, ne reflètent pas toujours l'impact complet de Compose. Plus une surface est migrée de bout en bout sans interruption de pontage, plus l'image devient claire et performante.
Les écrans principaux d'Instagram Direct sont conçus autour de longues listes de types d'éléments variés, initialement implémentées avec RecyclerView. L'architecture repose sur des abstractions personnalisées pour l'évolutivité, mais elle reste liée au cycle de vie du système basé sur les vues.
L'équipe s'est principalement chargée de migrer progressivement plusieurs centaines d'éléments de liste vers Compose au sein de l'architecture existante basée sur RecyclerView, en les déployant en production par petits groupes indépendants sous forme de tests A/B. Le tout sans que l'expérience de messagerie des utilisateurs ne soit visiblement modifiée.
Le principal inconvénient d'une telle configuration est une dépendance importante au système View hérité via une architecture RecyclerView de base, même après la migration complète de chaque élément de liste vers Compose. L'étape suivante logique pour l'équipe a été d'investir dans le remplacement de l'architecture de base basée sur RecyclerView par l'alternative native Compose : LazyColumn.
Cela signifie que les composants de l'interface utilisateur Compose doivent être abstraits du framework dans lequel ils sont inclus tout en restant compatibles avec RecyclerView et LazyColumn en même temps. Il est tout aussi important de pouvoir passer de l'un à l'autre au moment de l'exécution à l'aide de flags de fonctionnalité, pour permettre les tests A/B.
Alors que les nouveaux éléments Compose sont nativement compatibles avec LazyColumn et peuvent être insérés dans un arbre de composition ininterrompu, une API d'interopérabilité a été créée pour les insérer également dans un RecyclerView. Cela a permis de déployer la configuration LazyColumn dans un test A/B en parallèle de RecyclerView, en réutilisant les mêmes éléments Compose et en améliorant les performances, sans perturber le reste de l'équipe qui crée et affine les fonctionnalités.
L'échelle, la complexité et la sensibilité d'Instagram, même aux plus petites régressions, ont posé un défi unique à Jetpack Compose. Pour y remédier, nous avons dû établir un partenariat itératif et pratique. En travaillant en étroite collaboration, les ingénieurs de Google et de Meta ont analysé les métriques pour identifier et concevoir de nouvelles fonctionnalités Compose afin d'atteindre ou de dépasser les benchmarks basés sur les vues. Grâce à ce partenariat, les ajouts suivants à Jetpack Compose se distinguent : la composition pouvant être mise en pause avec LazyLayoutCacheWindows et le suivi de la visibilité.
Composition pouvant être mise en pause avec LazyLayoutCacheWindows
La composition pouvant être mise en pause (activée par défaut dans Compose 1.10) permet de composer des éléments de liste paresseux coûteux de manière incrémentielle sur plusieurs frames pour éviter les saccades. Associée à LazyLayoutCacheWindow (ajouté dans Compose 1.9), cette combinaison améliore considérablement la fluidité du défilement. Lors de tests internes récents chez Meta, la combinaison de la composition Pausable avec un LazyLayoutCacheWindow à une seule fenêtre d'affichage a permis de réduire les pertes d'images importantes par minute (LFD/m) d'environ 13% par rapport à Compose vanilla. La fenêtre de cache seule a permis de réduire la latence d'environ 8% par rapport à la même référence. LFD/m est une métrique interne utilisée par Meta pour suivre les saccades visibles lors du défilement.
L'utilisation d'un LazyLayoutCacheWindow dans votre application prépare et conserve les éléments hors écran dans une bande basée sur les pixels autour de la fenêtre d'affichage pour permettre des balayages rapides. Pour profiter de LazyLayoutCacheWindows dans votre application, vous pouvez utiliser la dernière version de Compose 1.13.0-alpha03 et la configurer comme indiqué dans l'exemple ci-dessous :
val cacheWindow = LazyLayoutCacheWindow(ahead = 150.dp, behind = 100.dp) // OR val cacheWindow = LazyLayoutCacheWindow(aheadFraction = 0.5f, behindFraction = 0.3f) LazyColumn(state = state, cacheWindow = cacheWindow) { ... }
Il existe deux façons de configurer la fenêtre de cache. Les deux décrivent la même chose : la quantité de contenu hors écran à conserver, mais dans des unités différentes.
- Dp : longueur absolue fixe.
ahead = 150.dpconserve 150 dp de contenu composé au-delà du bord visible, quel que soit l'appareil. - Float : fraction de la fenêtre d'affichage.
aheadFraction = 0.5fconserve une moitié d'écran composée à l'avance. La quantité absolue est donc proportionnelle à la hauteur de l'écran, ce qui permet de prendre en charge différents facteurs de forme : plus sur une tablette ou un appareil pliable déplié, moins sur un téléphone compact.
L'équipe Instagram a affiné les fractions flottantes de la fenêtre de cache spécifiquement pour la structure de contenu et la taille des éléments de Direct. Les valeurs idéales variant en fonction des paramètres spécifiques de l'UI, il est nécessaire de faire des tests pour trouver le juste équilibre.
Enregistrement des impressions avec onVisibilityChanged
L'API onVisibilityChanged (ajoutée dans Compose 1.9.0) est un autre résultat clé du partenariat technique entre Google et Meta. Il offre aux surfaces Jetpack Compose à grande échelle un moyen cohérent de savoir quand un composable est réellement visible à l'écran, en remplaçant les implémentations personnalisées et manuelles utilisées dans le passé. Dans Instagram Direct, ces signaux de visibilité sont utilisés dans des centaines de fichiers pour prendre en charge les métriques de qualité des produits qui dépendent de l'affichage ou non des éléments d'interface utilisateur aux utilisateurs.
Performances de démarrage
L'adoption de Jetpack Compose pour Instagram Direct a entraîné des améliorations inattendues des performances sur d'autres surfaces de l'application. L'exécution de Jetpack Compose entraîne un coût de préchauffage que vous ne payez qu'une seule fois. Comme la messagerie est une surface à fort trafic souvent visitée au début d'une session utilisateur, d'autres surfaces d'Instagram qui s'appuient sur Compose ont connu des améliorations notables des performances.
Les performances de démarrage de l'UI Compose dans Instagram Direct ont été optimisées grâce aux profils de référence, qui précompilent les chemins de code actifs au moment de l'installation. Compose s'affiche ainsi rapidement dès le premier lancement.
Leçons tirées de la migration d'Instagram Direct vers Jetpack Compose
- Jetpack Compose offre un retour sur investissement immédiat : vous n'avez pas besoin d'utiliser des workflows d'IA avancés pour bénéficier de Compose. La réduction de code d'environ 50% signifie qu'il y a moins de code à gérer et que la surface d'attaque des bugs est réduite.
- La conception d'une architecture native de l'IA a permis d'obtenir des résultats significatifs, y compris une réduction de 35% du temps d'exécution des agents d'IA, de 32% des échanges entre les ingénieurs et les agents, et de 33% du coût des jetons.
- Bien qu'il existe de nombreuses API d'interopérabilité et une compatibilité pour combiner les vues et Compose, essayez de migrer les grandes surfaces plutôt que les petits composants individuels. Cela permet de conserver l'UI dans une hiérarchie de composition unique et ininterrompue, et de débloquer toutes les meilleures optimisations de performances natives de Compose.
- Associez la composition Pausable à LazyLayoutCacheWindow : cette association donne de meilleurs résultats que les fenêtres de cache seules. Avec la fenêtre de cache uniquement, un élément lourd peut toujours essayer de composer en une seule passe, ce qui peut potentiellement dépasser le budget de frame.
- Contribuez à Compose ! Meta s'est associé à l'équipe Jetpack Compose pour concrétiser ses commentaires et ses idées dans Compose. En travaillant sur une boîte à outils Open Source, nous bénéficions tous des corrections de bugs et des améliorations de performances apportées de manière centralisée. Alors, faites-nous part de vos commentaires !
L'adoption de Jetpack Compose a permis de réaliser des gains importants dans le développement assisté par l'IA, tout en simplifiant l'ingénierie d'interface utilisateur quotidienne sur Instagram. L'approche déclarative réduit le code récurrent, facilite la compréhension de l'état et améliore la productivité globale des développeurs. L'équipe d'ingénierie d'Instagram a hâte d'intégrer Compose à davantage de surfaces de l'application, et de poursuivre sa collaboration avec Google pour apporter d'autres améliorations aux utilisateurs d'Instagram et de Jetpack Compose.
Si vous n'avez pas encore essayé Compose, qui est désormais assisté par l'IA, la migration vers Jetpack Compose est plus facile que jamais.
Remerciements. Merci à Michal Zielinski et Matthew Du de Meta, ainsi qu'à Andrei Shikov et George Mount de Google, pour leur travail visant à améliorer les performances de Compose grâce à la collaboration entre Meta et Google ! Merci également à Gary Ye de Meta pour avoir contribué à l'intégration de Compose à Instagram Direct, et à Gopal Juneja de Meta pour avoir soutenu cet effort grâce à la science des données !
-
Études de casWhatsApp est la plus grande plate-forme de messagerie au monde, avec des milliards d'utilisateurs dans le monde entier. Il s'agit de l'outil de communication par défaut pour les utilisateurs de différentes régions, qui permet de les mettre en relation grâce à une messagerie privée, fiable et sécurisée.
Niharika Arora, Tracy Agyemang, Mayank Jain • Temps de lecture : 8 min -
Études de casLa mission de Tinder est de favoriser et d'inspirer des relations authentiques en permettant à chaque nouvelle génération de célibataires de se rencontrer facilement et de s'amuser.
Ajesh Pai, Ulises Uriel Verduzco Díaz , Tracy Agyemang • Temps de lecture : 4 min -
Études de casLa majorité des applications Android adoptant Kotlin comme langage principal, kotlinx.coroutines est devenu une norme de facto pour la programmation asynchrone. La bibliothèque offre un moyen bien conçu et structuré de gérer les flux simultanés, natif à Kotlin.
Jonathan Starup, Andrei Shikov • Temps de lecture : 7 min
Recevez chaque semaine les dernières informations sur le développement Android directement dans votre boîte de réception.