Instagram Direct のエンジニアが Jetpack Compose を使用して AI ネイティブの UI アーキテクチャを構築し、エージェント セッションあたりのトークン費用を 33% 削減した方法
所要時間: 11 分
このブログ投稿は、Meta チームとの協力のもとで作成されました。
Instagram Direct は Instagram のコア サーフェスの 1 つで、毎日数十億件のユーザー メッセージを処理しています。チームは長年にわたるイテレーションを通じて、従来の Android View システムから可能な限りあらゆるマイクロ最適化を絞り出しました。しかし、高度に最適化されたレガシー サーフェスを維持、拡張すると、特にチームが宣言型 UI と AI コーディング アシスタントを導入するにつれて、技術的負債とエンジニアリング オーバーヘッドが大幅に増加します。
Instagram Direct への Jetpack Compose の導入は、一般的な UI のモダナイゼーションにとどまらず、チームは、元の実装よりも 50% 小さい AI ネイティブ UI コードベースを構築し、AI エージェントの実行時間を 35% 短縮、エンジニアとエージェントのやり取りを 32% 削減、トークン費用を 33% 削減しました。Google との緊密な連携のもと、チームは高いパフォーマンス基準を維持しながら Jetpack Compose を導入しました。パフォーマンスの最適化を通じて、Meta と Google は Instagram だけでなく、より広範な Android デベロッパー エコシステム向けに Compose を改善しました。
大規模なコードベースのモダナイゼーション
AI は、業界のエンジニアにとって急速に日常的なパートナーになりつつあり、Instagram のような大規模なコードベースに AI を適用することで、すでに生産性が実際に向上しています。Instagram Direct チームは、より野心的な目標を設定しました。チームは、単に AI ツールを既存のコードに適用するのではなく、コードベースとそのアーキテクチャを AI ネイティブになるように再設計しました。これにより、AI の効果は、単に後付けするだけの場合をはるかに上回るものになりました。
Instagram Direct チームは、AI ネイティブの UI アーキテクチャを構築するための重要なコンポーネントとして Jetpack Compose を選択しました。宣言型の性質により、コードは簡潔で予測可能になり、AI モデルが推論しやすい構造になります。また、副作用が少なく、暗黙的な状態が少なく、コンポーネントの境界が明確になります。
Jetpack Compose への移行には、慎重な計画が必要でした。毎日何億人ものユーザーが Instagram でメッセージを送信しているため、移行は段階的かつスムーズに行う必要があり、チームが基盤を再構築する間もユーザー エクスペリエンスを中断することはできませんでした。この課題の規模を説明すると、個々の UI コンポーネントは 160 を超える異なる状態の組み合わせでレンダリングでき、1 つの会話画面だけで 200 を超える異なるメッセージ タイプを処理します。
この規模のコードベースを Compose に移行する場合、簡単な方法を選択して、既存のビュー階層内に Compose UI コンポーネントを埋め込みたくなるかもしれません。段階的な移行のステップとしては、これは完全に有効です。しかし、長期的には、ビューベースのコードベース内に Compose を統合することは課題となります。AI ツールは、最も抵抗の少ない方法を選択することがよくあります。宣言型と命令型の 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 Direct サーフェスでは、Jetpack Compose により、UI コードの総量を 50%削減できました。AI が生成するコードが少ないほど、出力の品質が向上し、タスクあたりのトークン費用が削減されます。
Instagram Direct の Android コードベースの内部データ分析では、Compose UI で動作する AI エージェント セッションと、Android ビューを使用する同じタスクを比較しました。効率性の向上は、次の 2 つの側面で明らかになりました。
- 本番環境にデプロイされたコードの文字数あたり: Compose では、エンジニアとエージェントのやり取りが 32% 少なく、エージェントの実行時間(エージェントがエンジニアのリクエストの処理を開始してからレスポンスを返すまでの経過時間)が 35% 短縮されました。
- エージェント セッションあたり: Compose を使用した場合、Views と比較して全体的なトークン費用が 33% 削減されました。
出力効率と一般的なセッション数の両方を報告するのは、これらがそれぞれ独立して有用な結果であるためです。エンジニアとエージェントのやり取りと実行時間の数値は、出力単位あたりのリソース使用量を比較したもので、トークンの数値は、一般的なエージェント セッションの合計費用を比較したものです。
また、複雑なコードや脆弱なコードの処理方法において、2 つのフレームワークに一貫した違いがあることも明らかになりました。Meta は、コード変更のリスクスコアを使用してこれを追跡しています。このスコアは、コードの全体的な品質と、変更が本番環境でインシデントを引き起こす可能性を評価するものです。この分析では、トークン消費量、エージェントの実行時間、エンジニアとエージェントのやり取りを組み合わせて、エージェントのリソース効率を測定しました。ファイルのリスク スコアリングが高くなるにつれて、AI エージェント セッションのリソース効率は自然に低下します。
ファイルの累積リスクスコアが 2 倍になると、Android ビューで実装された UI により、エージェントのリソース効率が 30%低下します(着地したキャラクターあたり)。同じ状況で、 Jetpack Compose UI による削減はわずか 9%
Google と Meta のパートナーシップを通じて、Instagram Direct チームは Compose の導入に新たな視点をもたらしました。それは、単に UI を書き換えるだけでなく、コードベースの AI 対応という観点から Compose の導入にアプローチするというものです。この取り組みにより、Compose が AI ファーストのコードベースとアーキテクチャを構築するための基盤として、特に Instagram のような規模のアプリに適用された場合に、その強みを発揮することが明らかになりました。
パフォーマンスの最適化
Instagram Direct はアプリの最も重要なサーフェスの 1 つであり、ユーザーは常に高速で応答性が高いことを期待しています。Jetpack Compose を採用するということは、実質的に UI を大幅に書き換えることを意味します。そのため、回帰のない高品質なエクスペリエンスを維持することが最優先の目標でした。
Instagram の従来の View ベースの実装は、長年のイテレーションにより、すでに非常に高いパフォーマンス基準に達していました。チームは、まったく新しい UI フレームワークに移行しながら、同じ基準を満たす必要がありました。
Instagram では、数百、場合によっては数千のパフォーマンス指標が測定されます。Compose の導入にあたっては、次の 3 つが最も重要でした。
- 操作可能になるまでの時間 - 画面を開いてから操作可能になるまでの時間。
- 完全読み込みまでの時間 - 画面を開いてから、すべてのコンテンツ(画像など)が完全に読み込まれるまでの時間。
- スクロール パフォーマンス - フレーム落ちすることなく、画面がどれだけスムーズにスクロールするか。
これらの指標は本番環境のランタイムで追跡されるため、移行後の Compose UI と以前の UI を比較する A/B テストを実施して、この取り組みがパフォーマンスに与える影響を評価できます。
このような移行に取り組む一般的な方法は、小規模から始めて、少数の UI コンポーネントを移行し、データを収集して、それらの動作を調査することです。これらの初期の結果は有用ではあるものの、全体像の一部しか示しておらず、Compose の導入に対して偽陰性を示しています。その理由は次のとおりです。
- 代表性がない - 移行された 1 つの UI コンポーネントは、特定の画面での全体的なパフォーマンスに関する有用なデータを提供できます。ただし、コンポーネントによって動作が異なる理由は一般化できないため、常に外挿できるとは限りません。
- 相互運用コスト - 大規模な View コードベース内の小規模な Compose 部分では、2 つのシステム間で予測不可能なブリッジング コストが発生します。このオーバーヘッドによって測定値が歪むため、初期の小規模な結果は、完全な移行が実際にどのようになるかを反映していません。
その結果、小規模な移行は有用ではあるものの、Compose の効果を完全に反映しているとは限りません。 ブリッジによる中断なしにエンドツーエンドで移行されるサーフェスが多いほど、パフォーマンスの観点から画像が鮮明になり、画質が向上します。
Instagram Direct のコア画面は、さまざまな商品アイテム タイプの長いリストを中心に構築されており、元々は RecyclerView で実装されていました。このアーキテクチャは、スケーラビリティのためにカスタム抽象化に依存していますが、View ベースのシステムのライフサイクルにバインドされたままです。
チームの主な取り組みは、既存の RecyclerView ベースのアーキテクチャ内で、数百個のリストアイテムを Compose に段階的に移行することでした。これらのアイテムは、独立した小さなグループに分けて A/B テストを行い、本番環境にロールアウトしました。これらはすべて、ユーザーのメッセージング エクスペリエンスに目に見える変化を与えることなく行われました。
このような設定の最大のデメリットは、すべてのリストアイテムを Compose に完全に移行した後でも、コアの RecyclerView アーキテクチャを介して従来の View システムに大きく依存することです。自然な次のステップとして、チームは RecyclerView ベースのコア アーキテクチャを Compose ネイティブの代替手段である LazyColumn に置き換えることに投資することを決定しました。
つまり、Compose UI コンポーネントは、RecyclerView と LazyColumn の両方と同時に互換性を保ちながら、それらが含まれるフレームワークから抽象化される必要があります。同様に重要なのは、フィーチャー トグルを使用して実行時に 2 つのモデルを切り替え、A/B テストを可能にすることです。
新しい 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 分あたりの大きなフレーム落ち(LFDs/m)が約 13% 減少しました。キャッシュ ウィンドウ単体では、同じベースラインに対して約 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 API(Compose 1.9.0 で追加)は、Google と Meta の技術提携のもう一つの重要な成果です。これにより、大規模な Jetpack Compose サーフェスで、コンポーズ可能な関数が実際に画面に表示されるタイミングを把握する一貫した方法が提供され、過去に使用されていたカスタムの手動実装が置き換えられます。Instagram Direct だけでも、これらの視認性シグナルは数百のファイルで使用され、UI 要素が実際にユーザーに表示されたかどうかに依存するプロダクト品質指標をサポートしています。
起動時のパフォーマンス
Instagram Direct で Jetpack Compose を採用したことで、アプリの他のサーフェスでも予期せぬパフォーマンスの向上が見られました。Jetpack Compose ランタイムにはウォームアップ コストがかかりますが、これは 1 回だけ支払うものです。メッセージングはトラフィックが多く、ユーザー セッションの早い段階でアクセスされることが多いため、Compose を利用する Instagram の他のサーフェスでも顕著なパフォーマンスの向上が見られました。
Instagram Direct 内の Compose UI の起動パフォーマンス自体は、ベースライン プロファイルを使用して最適化されました。ベースライン プロファイルは、インストール時にホットコードパスをプリコンパイルするため、Compose は最初の起動から迅速にレンダリングされます。
Instagram Direct の Jetpack Compose への移行から得た教訓
- Jetpack Compose はすぐに投資収益率が得られる: Compose のメリットを享受するために、高度な AI ワークフローを使用する必要はありません。コードが約 50% 削減されたことで、メンテナンスが必要なコードが減り、バグの発生領域が縮小しました。
- AI ネイティブ アーキテクチャの設計により、大きな成果が得られました。たとえば、AI エージェントの実行時間が 35% 短縮され、エンジニアとエージェントのやり取りが 32% 減少し、トークン費用が 33% 削減されました。
- 相互運用 API が多数あり、ビューと Compose を組み合わせることもサポートされていますが、個々の小さなコンポーネントではなく、より大きなサーフェスを移行することを目指してください。これにより、UI が単一の、中断のないコンポジション階層内に維持され、Compose ネイティブのパフォーマンス最適化を最大限に活用できます。
- 一時停止可能なコンポジションと LazyLayoutCacheWindow を組み合わせる: この 2 つを組み合わせると、キャッシュ ウィンドウ単体よりも優れた結果が得られます。キャッシュ ウィンドウのみを使用する場合、重いアイテムは 1 回のパスでコンポーズしようとするため、フレーム予算を超過する可能性があります。
- Compose 自体に貢献するMeta は Jetpack Compose チームと提携し、フィードバックやアイデアを Compose 内で実現しています。オープンソースのツールキットを使用すると、バグの修正やパフォーマンスの改善が一元的に行われるため、すべてのユーザーがその恩恵を受けることができます。ぜひフィードバックをお寄せください。
Jetpack Compose を採用したことで、Instagram での日常的な UI エンジニアリングが簡素化されるとともに、AI を活用した開発のメリットが大幅に向上しました。宣言型のアプローチにより、ボイラープレートが削減され、状態の推論が容易になり、デベロッパーの全体的な生産性が向上します。Instagram のエンジニアリング チームは、アプリのより多くのサーフェスに Compose を導入すること、そして Google と Meta の継続的なコラボレーションにより、Instagram と Jetpack Compose のユーザーにさらなる改善をもたらすことを楽しみにしています。
Compose をまだお試しでない場合は、AI アシスタンスが追加された今、Jetpack Compose への移行がこれまで以上に簡単になっています。
謝辞。Meta と Google のコラボレーションを通じて Compose のパフォーマンス向上に貢献してくれた、Meta の Michal Zielinski 氏と Matthew Du 氏、Google の Andrei Shikov 氏と George Mount 氏に感謝いたします。また、Instagram Direct への Compose の導入に協力してくれた Meta の Gary Ye 氏と、データ サイエンスを通じてこの取り組みをサポートしてくれた Meta の Gopal Juneja 氏にも感謝いたします。
-
事例紹介Grab は、東南アジアを代表するスーパーアプリです。1 つのアプリで、8 か国の数百万人のユーザーにデリバリー、モビリティ、金融サービスを提供しています。
Ajesh Pai, Nguyen Van Minh, Tracy Agyemang • 所要時間: 5 分 -
事例紹介WhatsApp は世界最大のメッセージ プラットフォームであり、世界中の数十億人のユーザーにサービスを提供しています。さまざまな地域の人々にとってデフォルトのコミュニケーション ツールであり、プライベートで信頼性が高く、安全なメッセージングを通じてユーザーをつないでいます。
Niharika Arora, Tracy Agyemang, Mayank Jain • 所要時間: 8 分 -
事例紹介Tinder は、新しい世代の独身者が簡単かつ楽しく出会えるようにすることで、真のつながりを促進し、刺激を与えることを使命としています。
Ajesh Pai, Ulises Uriel Verduzco Díaz , Tracy Agyemang • 所要時間: 4 分
Android 開発に関する最新の分析情報を毎週メールでお届けします。