Instagram ダイレクトのエンジニアが Jetpack Compose で AI ネイティブの UI アーキテクチャを構築し、エージェント セッションあたりのトークン費用を 33% 削減した方法
所要時間: 11 分
このブログ投稿は Meta チームとの共同執筆です
Instagram ダイレクトは、Instagram の主要なサーフェスの 1 つであり、毎日数十億件のユーザー メッセージを処理しています。長年のイテレーションを経て、チームは従来の Android View システムから可能な限りのマイクロ最適化を絞り出しました。しかし、高度に最適化されたレガシー サーフェスを維持し、拡張すると、特にチームが宣言型 UI や AI コーディング アシスタントをますます採用するようになるにつれて、技術的負債とエンジニアリング オーバーヘッドが大幅に増大します。
Instagram ダイレクトに Jetpack Compose を採用したことは、単なる UI の最新化にとどまりませんでした。チームは、元の実装よりも 50% 小さい AI ネイティブ UI コードベースを構築し、AI エージェントの実行時間を 35% 短縮、エンジニアとエージェントのやり取りを 32% 削減、トークン費用を 33% 削減しました。Google との緊密なパートナーシップのもと、高いパフォーマンス基準を維持しながら Jetpack Compose を採用しました。パフォーマンスの最適化を通じて、Meta と Google は Instagram だけでなく、より広範な Android デベロッパー エコシステム向けにも Compose を改善しました。
大規模なコードベースのモダナイゼーション
AI は業界のエンジニアにとって急速に日常的なパートナーとなり、Instagram のような大規模なコードベースに適用することで、すでに生産性の向上という成果を上げています。Instagram ダイレクト チームは、より野心的な目標を設定しました。チームは、既存のコードに AI ツールを適用するだけでなく、コードベースとそのアーキテクチャを AI ネイティブになるように再設計し、AI の影響をレトロフィットだけでは実現できないレベルにまで高めました。
Instagram Direct チームは、AI ネイティブ UI アーキテクチャを構築するための重要なコンポーネントとして Jetpack Compose を選択しました。宣言型であるため、コードは簡潔で予測可能であり、AI モデルが推論しやすい構造になっています。副作用が少なく、暗黙的な状態が少なく、コンポーネントの境界が明確です。
Jetpack Compose への移行には慎重な計画が必要でした。毎日何億人ものユーザーが Instagram でメッセージを送信しているため、チームが基盤を再構築している間、ユーザー エクスペリエンスを中断することなく、移行を段階的にスムーズに進める必要がありました。この課題の規模を説明すると、個々の UI コンポーネントは 160 以上の異なる状態の順列でレンダリングでき、1 つの会話画面だけで 200 以上の異なるメッセージ タイプを処理します。
このサイズのコードベースを Compose に移行する場合、既存のビュー階層内に Compose UI コンポーネントを埋め込むという簡単な方法を取りたくなるかもしれません。段階的な移行の増分ステップとしては、完全に有効です。ただし、長期的には、ビューベースのコードベースに Compose を統合することは困難です。AI ツールは、多くの場合、最も手間のかからない方法を選択します。宣言型 UI コードと命令型 UI コードを混在させると、AI がそれらを誤ってブレンドし、微妙なバグ、技術的負債、パフォーマンスの低下が発生する可能性があります。
AI ネイティブな UI アーキテクチャを構築する
Instagram の規模では、アーキテクチャの抽象化は避けられません。また、アプリの成長に合わせて保守性を維持するためにも必要です。一般的なパターンとして、すべての RecyclerView アイテムタイプが、onBind などの通常のライフサイクル フックを公開するカスタム RecyclerViewItem 基底クラスの子孫としてモデル化されている場合を考えてみましょう。
例 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") } } ... } } }
上のスニペットには、2 つの問題があります。まず、命令型コードで isPinnedChatsEnabled フラグが読み取られ、Compose ラムダ内でキャプチャされます。これは、パラダイム間の微妙な結合です。第二に、isPinned は ChatUiState ではなくアイテム自体に変更可能なフィールドとして存在するため、RecyclerView の再バインドと行間のリサイクルを生き残り、再現が困難なリークやバグが発生します。
アイテムに専用の @Composable 関数を付与してコードをクリーンアップしても、同じ問題が残ります。
例 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") } } ... } }
これは意図的に単純な例ですが、より広範な問題を示しています。AI に与えられる境界が少ないほど、生成されるコードの品質は時間の経過とともに低下します。ガードレールとスキルは役立ちますが、それだけでは十分ではありません。AI が摩擦に直面すると、多くの場合、その摩擦を回避してブロックを解除するためです。
コードベースを AI に適したものにするには、次の 2 つの実用的なルールに従う必要があります。
- カスタム コンテキストへの依存を最小限に抑えます。AI エージェントが正しい変更を行うために必要な、コードベース固有の知識が多ければ多いほど、出力の品質は低下します。コードベースが既知のベスト プラクティスに近いほど、AI の結果は良くなります。
- AI ファーストのコードベースは、独自の境界を適用する必要があります。AI スキルで設計上のギャップを埋めることはスケーラブルではありません。コンテキストに読み込まれるスキルごとにトークン費用が発生し、エージェントのパフォーマンスが低下する可能性があるためです。代わりに、アーキテクチャ自体がその重みを担う必要があります。AI エージェントは自然に抵抗の少ない道を進むため、その道が正しく高品質なコードにつながるように設計する必要があります。一方、不適切な設計上の決定は表現が難しく、コストがかかるようにする必要があります。
リスト項目は独自の抽象化で表すことができますが、この場合、すべての Compose コードはコンストラクタに存在するため、クラス メンバーや状態にアクセスできず、引数の唯一のソースはコンストラクタになります。これにより、既存のアーキテクチャに準拠しながら、通常の @Composable 関数と同等の機能を実現できます。
例 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") } } ... }, )
このサイズのコードベースを移行するのは、大規模な作業です。長い間、Direct UI の大部分を構成する数百もの UI コンポーネントは、レガシーの対応コンポーネントと共存し、両方が並行して維持されてきました。AI ワークフローは、大量のコードの作成プロセスを高速化することで、この並行移行を可能にしました。このアプローチにより、Direct チームは記録的な速さで移行を完了できました。その間、他のチームは中断されることなく、毎日何百万人ものユーザーの利便性を高める機能をリリースし続けました。
複数のエンジニアが、移行中に構築された再利用可能なスキルと規約の共有ナレッジベースに対して、独自の AI エージェントを実行しました。これにより、各エンジニアがワークフローとベスト プラクティスを再発見するのではなく、チーム全体で同期を維持できるようになりました。各サーフェス内で、チームは次の段階で移行を実施しました。
- AI を使用してすべての Compose コードを記述します。
- エッジケースを処理し、パフォーマンスのギャップを埋めながら、UI が公開テストで実際のユーザーにロールアウトされるまで磨き上げました。
画面ごとに 2 つのステージに分割することで、1 人のエンジニアがサーフェス全体をすばやく移動し、アーキテクチャと難しいエッジケースを事前に解決できます。この基盤が整っていれば、他のユーザーは技術的な決定を自分で下す必要がなく、UI を本番環境に対応させることに集中できるため、移行全体を迅速に進めることができます。
移行の結果により、このアプローチが検証されました。移行した Instagram ダイレクト サーフェスでは、Jetpack Compose により、チームはUI コードの総量を 50%削減できました。AI が生成するコードが少ないほど、出力の品質が高くなり、タスクあたりのトークン費用が低くなります。
Instagram Direct の Android コードベースの内部データ分析では、Compose UI で動作する AI エージェント セッションと、Android Views を使用した同じタスクを比較しました。効率性の向上は、次の 2 つの側面で明らかでした。
- コード 1 文字あたり: Compose を使用すると、エンジニアとエージェントのやり取りが 32% 減少し、エージェントの実行時間が 35% 減少します(エージェントがエンジニアのリクエストの処理を開始してからレスポンスを返すまでの経過時間)。
- エージェント セッションあたり: Compose を使用すると、Views と比較してトークン費用が全体で 33% 削減されました。
出力効率と一般的なセッション数の両方をレポートするのは、それぞれが独立して有用な結果であるためです。エンジニア エージェントの交換と実行時間の数値は、ランディングされた出力の単位あたりのリソース使用量を比較します。一方、トークンの数値は、一般的なエージェント セッションの総費用を比較します。
また、複雑なコードや脆弱なコードの処理方法に一貫した違いがあることも明らかになりました。Meta は、コード変更のリスクスコアを使用してこれを追跡します。このスコアは、コードの全体的な品質と、変更によって本番環境でインシデントが発生する可能性を評価します。この分析では、トークン消費量、エージェントの実行時間、エンジニアとエージェントのやり取りの複合指標を使用して、エージェントのリソース効率を測定しました。ファイルのリスクスコアが高くなるにつれて、AI エージェント セッションのリソース効率は自然に低下します。
ファイルの累積リスクスコアが 2 倍になると、Android Views で実装された UI はエージェントのリソース効率を 30%低下させます(着地した文字ごと)。同じ状況で、 Jetpack Compose UI による削減はわずか 9% です。
Google と Meta のパートナーシップを通じて、Instagram Direct チームは Compose の導入に新たな視点をもたらしました。単なる UI の書き換えではなく、コードベースの AI 対応という観点からアプローチしたのです。この取り組みにより、Compose が AI ファーストのコードベースとアーキテクチャを構築するための基盤として機能する強みが明らかになりました。特に、Instagram のような大規模なアプリに適用した場合にその強みが発揮されます。
パフォーマンスの最適化
Instagram ダイレクトはアプリの最も重要なサーフェスの 1 つであり、ユーザーは常に高速で応答性が高いことを期待しています。Jetpack Compose を採用することは、実質的に UI の大幅な書き換えを意味し、回帰のない高品質なエクスペリエンスを維持することが最優先事項でした。
長年のイテレーションにより、Instagram のレガシーの View ベースの実装はすでに非常に高いパフォーマンス基準に達しており、チームはまったく新しい UI フレームワークに移行しながら、その基準を満たす必要がありました。
Instagram では、数百、数千ものパフォーマンス指標を測定しています。Compose の導入では、次の 3 つが最も重要でした。
- 操作可能になるまでの時間 - 画面を開いてから操作可能になるまでの時間。
- 完全読み込みまでの時間 - 画面を開いてからすべてのコンテンツ(画像など)が完全に読み込まれるまでの時間。
- スクロール パフォーマンス - フレーム落ちなしで画面がスムーズにスクロールされるかどうか。
これらの指標は本番環境で実行時に追跡されるため、移行された Compose UI と以前の UI を比較する A/B テストを実行し、この取り組みのパフォーマンスへの影響を評価できます。
このような移行を行う一般的な方法は、少数の UI コンポーネントを移行し、データを収集して、それらの動作を調査することから始めることです。これらの初期結果は参考になりますが、Compose の導入に対する偽陰性を示すため、全体像を把握するには不十分です。
- 代表的ではない - 移行された 1 つの UI コンポーネントが、特定の画面での全体的なパフォーマンスに関する有用なデータを提供できます。ただし、コンポーネントによって動作が異なるため、常に外挿できるとは限りません。
- 相互運用コスト - 大きな View コードベース内の小さな Compose 部分が、2 つのシステム間の予測不可能なブリッジング コストを支払います。このオーバーヘッドによって測定値が歪められるため、初期の小規模な結果は、完全な移行が実際にどのようなものになるかを反映していません。
そのため、小規模な移行は有用ですが、Compose の効果を完全に反映するとは限りません。 中断を伴うブリッジングなしでサーフェス全体を移行するほど、パフォーマンスの面でより明確で優れた結果が得られます。
Instagram ダイレクトのコア画面は、さまざまなアイテムタイプの長いリストを中心に構築されており、元々は RecyclerView で実装されていました。このアーキテクチャは、スケーラビリティのためにカスタム抽象化に依存していますが、View ベースのシステムのライフサイクルにバインドされたままです。
チームの主な取り組みは、既存の RecyclerView ベースのアーキテクチャ内で、数百個の個々のリストアイテムを Compose に段階的に移行し、A/B テストの下で小さな独立したグループで本番環境にロールアウトすることでした。これらすべてを、ユーザーのメッセージ エクスペリエンスに目に見える変更を加えることなく行いました。
このような設定の最大の欠点は、すべてのリストアイテムを Compose に完全に移行した後でも、コアの RecyclerView アーキテクチャを通じてレガシーの View システムに大きく依存することです。チームは、次のステップとして、RecyclerView ベースのコア アーキテクチャを Compose ネイティブの代替手段である LazyColumn に置き換えることにしました。
つまり、Compose UI コンポーネントは、RecyclerView と LazyColumn の両方と同時に互換性を保ちながら、囲まれているフレームワークから抽象化される必要があります。同様に重要なのは、A/B テストを可能にするために、機能フラグを介して実行時に 2 つを切り替える機能です。
新しい Compose アイテムは LazyColumn とネイティブに互換性があり、中断のないコンポジション ツリーにプラグインできますが、それらを RecyclerView にもスロットインできるように、相互運用 API が作成されました。これにより、RecyclerView と並行して A/B テストで LazyColumn の設定をロールアウトできるようになりました。同じ Compose アイテムを再利用し、パフォーマンスを向上させながら、チームの他のメンバーが機能の構築と改善を妨げることなく進めることができます。
Instagram の規模、複雑さ、わずかな回帰に対する感度は、Jetpack Compose に独自の課題をもたらしました。これらの課題に対処するには、反復的な、実践的パートナーシップが必要でした。Google と Meta のエンジニアは緊密に連携して指標を分析し、View ベースのベンチマークを満たすか上回る新しい Compose 機能を特定して設計しました。このパートナーシップの結果、Jetpack Compose には LazyLayoutCacheWindows による一時停止可能なコンポジションと可視性トラッキングという顕著な追加機能が加わりました。
LazyLayoutCacheWindows を使用した一時停止可能なコンポジション
一時停止可能なコンポジション(Compose 1.10 でデフォルトで有効)により、高コストの遅延リスト項目をフレーム間で段階的にコンポーズして、ジャンクを防ぐことができます。LazyLayoutCacheWindow(Compose 1.9 で追加)と組み合わせると、スクロールの滑らかさが大幅に向上します。Meta で最近実施された社内テストでは、一時停止可能なコンポジションと 1 つのビューポート LazyLayoutCacheWindow を組み合わせることで、バニラ Compose と比較して、1 分あたりの大きなフレーム落ち(LFD/m)が約 13% 減少しました。Cache Window 単体では、同じベースラインに対して約 8% の削減となりました。LFDs/m は、スクロール中に目立つカクつきを追跡するために Meta が使用する内部指標です。
アプリで LazyLayoutCacheWindow を使用すると、ビューポートの周囲のピクセルベースの帯域内で画面外のアイテムが準備され保持されるため、高速フリングが可能になります。アプリで LazyLayoutCacheWindows を活用するには、最新の Compose 1.13.0-alpha03 を使用し、次の例のように設定します。
val cacheWindow = LazyLayoutCacheWindow(ahead = 150.dp, behind = 100.dp) // OR val cacheWindow = LazyLayoutCacheWindow(aheadFraction = 0.5f, behindFraction = 0.3f) LazyColumn(state = state, cacheWindow = cacheWindow) { ... }
キャッシュ ウィンドウを構成する方法は 2 つあります。どちらも同じことを表しています。画面外のコンテンツをどの程度コンポーズしておくかを表しますが、単位が異なります。
- Dp: 固定の絶対長。
ahead = 150.dpは、デバイスに関係なく、表示されているエッジを超えてコンポーズされたコンテンツを 150 dp 保持します。 - Float: ビューポートの割合。
aheadFraction = 0.5fは、画面の半分を事前に構成しておくため、絶対量は画面の高さに応じてスケーリングされ、さまざまなフォーム ファクタに対応します。タブレットや折りたたみ式デバイスを開いた状態では多く、コンパクトなスマートフォンでは少なくなります。
Instagram チームは、Direct のコンテンツ構造とアイテムサイズに合わせて、キャッシュ ウィンドウの浮動小数点数を微調整しました。理想的な値は特定の UI パラメータによって異なるため、適切なバランスを見つけるにはある程度のテストが必要です。
onVisibilityChanged を使用したインプレッションのロギング
onVisibilityChanged (Compose 1.9.0 で追加)API は、Google と Meta の技術提携のもう 1 つの重要な成果です。大規模な Jetpack Compose サーフェスで、コンポーザブルが実際に画面に表示されるタイミングを把握するための統一された方法を提供し、過去に使用されていたカスタムの手動実装を置き換えます。Instagram ダイレクトだけでも、これらの可視性シグナルは数百ものファイルで使用され、UI 要素が実際にユーザーに表示されたかどうかに依存するプロダクト品質指標をサポートしています。
起動時のパフォーマンス
Instagram ダイレクトに Jetpack Compose を採用したことで、アプリの他のサーフェスでも予期しないパフォーマンスの改善が見られました。Jetpack Compose ランタイムには、1 回限りのウォームアップ コストがかかります。メッセージングは、ユーザー セッションの早い段階で頻繁にアクセスされるトラフィックの多いサーフェスであるため、Compose に依存する Instagram の他のサーフェスでパフォーマンスの著しい改善が見られました。
Instagram ダイレクト内の Compose UI の起動パフォーマンスは、ベースライン プロファイルを使用して最適化されました。これにより、インストール時にホットコードパスが事前にコンパイルされるため、Compose は初回起動時から迅速にレンダリングされます。
Instagram Direct の Jetpack Compose への移行から得られた教訓
- Jetpack Compose はすぐに投資収益率が向上します。Compose のメリットを享受するために、高度な AI ワークフローを使用する必要はありません。コードが約 50% 削減されたため、メンテナンスするコードが減り、バグの発生する可能性も低くなります。
- AI ネイティブ アーキテクチャの設計により、AI エージェントの実行時間が 35% 短縮、エンジニアとエージェントのやり取りが 32% 削減、トークン費用が 33% 削減されるなど、大きな成果が得られました。
- 相互運用 API や、View と Compose を組み合わせるためのサポートは豊富にありますが、個々の小さなコンポーネントではなく、大きなサーフェスを移行することを目指してください。これにより、UI は単一の途切れないコンポジション階層内に維持され、Compose ネイティブのパフォーマンス最適化をすべて利用できるようになります。
- 一時停止可能なコンポジションと LazyLayoutCacheWindow を組み合わせる: この 2 つを組み合わせると、キャッシュ ウィンドウ単独よりも良い結果が得られます。キャッシュ ウィンドウのみの場合、重いアイテムは 1 回のパスでコンポーズを試み、フレーム バジェットを超える可能性があります。
- Compose 自体に貢献するMeta は Jetpack Compose チームと提携し、フィードバックやアイデアを Compose 内で実現しています。オープンソースのツールキットに取り組むことで、バグの修正やパフォーマンスの改善が中央で行われるため、すべてのユーザーがメリットを享受できます。フィードバックをお寄せください。
Jetpack Compose を採用したことで、Instagram では AI 支援開発が大幅に改善され、日々の UI エンジニアリングが簡素化されました。宣言型アプローチにより、ボイラープレートが減り、状態を推論しやすくなり、デベロッパーの生産性が全体的に向上します。Instagram エンジニアリング チームは、Compose をアプリのさまざまなサーフェスに導入することを楽しみにしています。また、Google と Meta の継続的なコラボレーションにより、Instagram と Jetpack Compose の両方のユーザーにさらなる改善をもたらすことを期待しています。
まだ Compose をお試しでない場合は、AI アシスタントを利用して、これまで以上に簡単に Jetpack Compose に移行できます。
謝辞。Meta の Michal Zielinski 氏と Matthew Du 氏、Google の Andrei Shikov 氏と George Mount 氏に感謝いたします。Meta と Google のコラボレーションを通じて、Compose のパフォーマンス改善を実現していただきました。また、Compose を Instagram ダイレクトに導入するにあたりご協力いただいた Meta の Gary Ye 氏、データ サイエンスを通じてこの取り組みをサポートしてくださった Meta の Gopal Juneja 氏にも感謝いたします。
-
事例紹介WhatsApp は世界最大のメッセージ プラットフォームであり、世界中の数十億人のユーザーにサービスを提供しています。さまざまな地域の人々にとってデフォルトのコミュニケーション ツールであり、プライベートで信頼性が高く安全なメッセージングを通じてユーザーを結び付けます。
Niharika Arora, Tracy Agyemang, Mayank Jain • 所要時間: 8 分 -
事例紹介Tinder の使命は、新しい世代のシングルが簡単かつ楽しく出会えるようにすることで、真のつながりを促進し、インスピレーションを与えることです。
-
事例紹介Android アプリの大部分で Kotlin がメインの言語として採用されているため、kotlinx.coroutines は非同期プログラミングの事実上の標準となっています。このライブラリは、Kotlin ネイティブの同時実行フローを管理するための、適切に設計された構造化された方法を提供します。
Jonathan Starup, Andrei Shikov • 所要時間: 7 分
Android 開発に関する最新の分析情報を毎週メールでお届けします。