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 ラムダ内でキャプチャされます。これは、パラダイム間の微妙な結合です。2 つ目は、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 をロールアウトするまで、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 が AI ファーストのコードベースとアーキテクチャを構築するための基盤として、特に Instagram のような規模のアプリに適用された場合に、その強みを発揮することが明らかになりました。
パフォーマンスの最適化
Instagram Direct はアプリの最も重要なサーフェスの 1 つであり、ユーザーは常に高速で応答性が高いことを期待しています。Jetpack Compose を採用するということは、実質的に UI を大幅に書き換えることを意味します。その際の最優先目標は、回帰のない高品質なエクスペリエンスを維持することでした。
Instagram では、長年にわたるイテレーションにより、従来のビューベースの実装がすでに非常に高いパフォーマンス基準に達していました。チームは、まったく新しい UI フレームワークに移行しながら、同じ基準を満たす必要がありました。
Instagram では、数百、場合によっては数千のパフォーマンス指標が測定されます。Compose の導入にあたっては、次の 3 つが最も重要でした。
- Time to Interactive - 画面を開いてから操作可能になるまでの時間。
- 完全な読み込みにかかる時間 - 画面を開いてから、すべてのコンテンツ(画像など)が完全に読み込まれるまでの時間。
- スクロール パフォーマンス - フレーム落ちすることなく、画面がどれだけスムーズにスクロールするか。
これらの指標は本番環境のランタイムで追跡されるため、移行後の 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 が作成されました。これにより、LazyColumn の設定を A/B テストで RecyclerView と並行してロールアウトできるようになりました。同じ 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(Compose 1.9.0 で追加)API は、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 氏にも感謝いたします。
-
事例紹介銀行や SIM 登録用の本人確認アプリでは、なりすましやディープフェイクを検出するために、鮮明で高解像度の顔画像が必要です。ライブネス チェックやなりすまし防止チェックに十分な鮮明さのフレームを、メモリの少ないデバイスをクラッシュさせることなくキャプチャすることは、Android カメラ デベロッパーにとって共通の課題です。
Donovan McMurray • 所要時間: 4 分 -
事例紹介Grab は、東南アジアを代表するスーパーアプリです。1 つのアプリで、8 か国の数百万人のユーザーにデリバリー、モビリティ、金融サービスを提供しています。
Ajesh Pai, Nguyen Van Minh, Tracy Agyemang • 所要時間: 5 分 -
事例紹介WhatsApp は世界最大のメッセージ プラットフォームであり、世界中の数十億人のユーザーにサービスを提供しています。さまざまな地域のユーザーがデフォルトのコミュニケーション ツールとして使用しており、プライベートで信頼性が高く、安全なメッセージングを通じてユーザー同士をつなげています。
Niharika Arora, Tracy Agyemang, Mayank Jain • 所要時間: 8 分
Android 開発に関する最新の分析情報を毎週メールでお届けします。