個案研究

Instagram Direct 工程師如何使用 Jetpack Compose 建構 AI 原生 UI 架構,並將每個代理工作階段的權杖成本降低 33%

閱讀時間:11 分鐘

這篇網誌文章是與 Meta 團隊合作撰寫

Instagram Direct 是 Instagram 的核心介面之一,每天處理數十億則使用者訊息。經過多年的反覆調整,團隊已盡可能對舊版 Android View 系統進行微幅最佳化。不過,維護及擴充高度最佳化的舊版介面,會產生大量技術債和工程負擔,尤其是在團隊越來越常採用宣告式 UI 和 AI 程式開發輔助的情況下。

Instagram Direct 採用 Jetpack Compose,不只是為了翻新 UI,團隊建構了 AI 原生 UI 程式碼集,比原始實作縮減 50% ,同時縮短 35% 的 AI 代理執行時間、減少 32% 的工程師與代理互動,並降低 33% 的權杖成本。該團隊與 Google 密切合作,採用 Jetpack Compose,同時維持高效能標準。透過效能最佳化,Meta 和 Google 不僅改善了 Instagram 的 Compose,也改善了更廣泛的 Android 開發人員生態系統。

大規模翻新程式碼集

AI 已迅速成為業界工程師的日常夥伴,將其應用於 Instagram 等大型程式碼集,已帶來實際的效率提升。Instagram Direct 團隊設定了更遠大的目標。團隊並非只是將 AI 工具套用至現有程式碼,而是重新設計程式碼集和架構,使其原生支援 AI,讓 AI 發揮遠超單純改造的效果。

Instagram Direct 團隊選擇 Jetpack Compose 做為建構 AI 原生 UI 架構的重要元件。宣告式特性可確保程式碼簡潔、可預測,且結構上更容易讓 AI 模型推論,同時減少副作用和隱含狀態,並明確劃分元件界線。

遷移至 Jetpack Compose 需要仔細規劃。每天有數億人在 Instagram 上傳送訊息,因此遷移作業必須逐步、順暢地進行,在團隊重新架構基礎時,不能中斷使用者體驗。為了說明這項挑戰的規模:個別 UI 元件可以轉譯超過 160 種不同的狀態排列組合,而單一對話畫面則可處理超過 200 種不同的訊息類型。

Product Design 1.png

將這種規模的程式碼集遷移至 Compose 時,您可能會想採取簡單的做法,將 Compose UI 元件嵌入現有的檢視區塊階層中。在逐步遷移的過程中,這是完全可行的做法。不過,長期來看,在以 View 為基礎的程式碼集中整合 Compose 是一項挑戰。AI 工具通常會選擇阻力最小的路徑。如果混合使用宣告式和指令式 UI 程式碼,AI 可能會錯誤地混合兩者,導致細微錯誤、技術債和效能降低。

建構 AI 原生 UI 架構

以 Instagram 的規模來說,一定程度的架構抽象化是不可避免的,這也是應用程式在成長過程中能保持可維護性的原因。請考慮常見的模式,其中每個 RecyclerView 項目類型都會模擬為自訂 RecyclerViewItem 基礎類別的子系,該類別會公開 onBind 等一般生命週期掛鉤。

示例 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")
          }
        }
        
        ...
      }
  }
}


在上方的程式碼片段中,有兩個問題。首先,系統會在指令式程式碼中讀取 isPinnedChatsEnabled 旗標,然後在 Compose lambda 中擷取,這是跨範式的細微耦合。其次,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 使用,請遵循下列兩項實用規則:

  • 盡量減少對自訂情境的依賴。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 在公開測試中向實際使用者推出。

將每個畫面的工作分成兩個階段,可讓一位工程師快速完成整個介面,預先解決架構和棘手的極端案例。有了這項基礎,其他人就能專注於讓 UI 可用於正式環境,不必停下來自行做出技術決策,進而加快整體遷移速度。

遷移結果證實了這個做法的成效。對於已遷移的 Instagram Direct 介面,Jetpack Compose 讓團隊減少 50%的 UI 程式碼總量。AI 生成的程式碼越少,輸出內容品質越高,每項工作的權杖成本也越低。

Quote-Pavlo-New.jpg

我們對 Instagram Direct 的 Android 程式碼集進行內部資料分析,比較 AI 代理在 Compose UI 上處理工作階段,與使用 Android View 處理相同工作的成效。效率提升在兩個方面顯而易見:

  • 每字元落地程式碼:Compose 減少 32% 的工程師與代理互動,以及 35% 的代理執行時間(從代理開始處理工程師要求到傳回回應的經過時間)。
  • 每個代理工作階段:與 View 相比,Compose 的整體詞元成本降低了 33% 。

我們同時提供輸出效率和一般工作階段數,因為這兩項結果都很有用。工程師與代理之間的互動和執行時間數據,比較的是每單位產出的資源用量,而權杖數據則比較一般代理工作階段的總成本。

資料也顯示,這兩個架構在處理複雜或脆弱的程式碼時,表現有明顯差異。Meta 透過程式碼變更的風險評分來追蹤這項指標,評估整體程式碼品質,以及變更導致正式環境事件的可能性。這項分析綜合考量權杖用量、代理執行時間,以及工程師與代理的互動,評估代理的資源效率。隨著檔案累積的風險評分提高,AI 代理工作階段的資源效率自然會降低。

當檔案的累積風險評分加倍時,使用 Android View 實作的 UI 會將代理程式資源效率降低 30% (每個落地字元)。在相同情況下, Jetpack Compose UI 僅減少 9%

Google 與 Meta 攜手合作,Instagram Direct 團隊也為 Compose 採用帶來全新觀點,從程式碼集是否支援 AI 的角度切入,而不只是重寫 UI。這項工作顯示 Compose 的優勢,可做為建構 AI 優先程式碼集和架構的基礎,尤其是在 Instagram 等應用程式規模上。

效能最佳化 

Instagram Direct 是應用程式中最重要的介面之一,使用者希望它隨時都能快速回應。採用 Jetpack Compose 實際上需要大幅重寫 UI,而首要目標是維持高品質體驗,不發生迴歸。

經過多年的疊代,Instagram 舊有的 View 實作方式已達到極高的效能標準,團隊在改用全新的 UI 架構時,也必須達到相同的標準。

Instagram 會評估數百甚至數千項成效指標。在採用 Compose 時,以下三項是最重要的考量:

  • 互動時間:從開啟畫面到可使用畫面之間的時間長度。
  • 完整載入所需時間 :從開啟畫面到所有內容 (例如圖片) 完整載入所需的時間。
  • 捲動效能:螢幕捲動的流暢度,不會發生影格遺失。

這些指標會在實際工作環境中執行時追蹤,因此可以執行 A/B 測試,比較遷移後的 Compose UI 與舊版 UI,並評估這項作業對效能的影響。

這類遷移作業的常見做法是從小處著手,先遷移少數 UI 元件、收集資料,並研究其行為。雖然這些早期結果很有幫助,但只能反映部分情況,並會針對 Compose 採用提供偽陰性,原因如下:

  • 不具代表性:遷移後的 UI 元件可提供實用資料,說明其在特定畫面上的整體成效。不過,不同元件的行為會因無法歸納的原因而有所不同,因此不一定能從中推斷。
  • 互通成本:在大型 View 程式碼集中,小型 Compose 部分會產生無法預測的系統間橋接成本。這項額外負擔會扭曲評估結果,因此早期的少量結果無法反映完整遷移的實際情況。

因此,小規模遷移雖然實用,但不一定能完全反映 Compose 的影響。 如果能端對端遷移更多介面,而不中斷橋接,就能更清楚、更有效地提升效能。

Instagram Direct 的核心畫面是圍繞著各種項目類型的長清單建構而成,最初是以 RecyclerView 實作。這個架構仰賴自訂抽象化機制來提高擴充性,但仍受限於 View 系統的生命週期。

Diagram 1.png

該團隊的主要任務是將數百個個別清單項目逐步遷移至現有 RecyclerView 架構中的 Compose,並在 A/B 測試中以小型獨立群組的形式在實際工作環境中推出,而這一切都不會對使用者的訊息體驗造成明顯變化。

這種設定的最大缺點是,即使所有清單項目都已完全遷移至 Compose,仍會透過核心 RecyclerView 架構高度依賴舊版 View 系統。接下來,團隊決定投資將以 RecyclerView 為基礎的核心架構,替換為 Compose 原生替代方案 LazyColumn。

也就是說,Compose UI 元件應從其所在的架構中抽象化,同時仍與 RecyclerView 和 LazyColumn 相容。同樣重要的是,您可以在執行階段透過功能旗標切換這兩種模式,以便進行 A/B 測試。

Diagram 2.png

雖然新的 Compose 項目原生與 LazyColumn 相容,且可插入不中斷的組合結構樹,但我們也建立了互通 API,可將這些項目插入 RecyclerView。這樣一來,就能在 A/B 測試中同時推出 LazyColumn 設定和 RecyclerView,重複使用相同的 Compose 項目並提升效能,而不會干擾團隊其他成員建構及調整功能。

Instagram 的規模、複雜度和對細微迴歸的敏感度,對 Jetpack Compose 來說都是獨特的挑戰。為解決這些問題,我們需要反覆實際合作。Google 和 Meta 工程師密切合作,分析指標,找出並設計新的 Compose 功能,以達到或超越以 View 為基礎的基準。這項合作促成了 Jetpack Compose 的幾項重要新增功能:可暫停的組合結構 (使用 LazyLayoutCacheWindows) 和瀏覽權限追蹤。

使用 LazyLayoutCacheWindows 暫停組合

可暫停組合 (Compose 1.10 預設啟用) 可讓耗費大量資源的 Lazy 清單項目在多個影格中逐步組合,避免卡頓。搭配 LazyLayoutCacheWindow (Compose 1.9 新增) 使用時,可大幅提升捲動流暢度。Meta 最近的內部測試顯示,與一般 Compose 相比,將可暫停組合與單一可視區域 LazyLayoutCacheWindow 結合,可將每分鐘大型影格掉格數 (LFD/m) 減少約 13%。單獨使用快取視窗時,與相同基準相比,延遲時間減少約 8%。LFDs/m 是 Meta 的內部指標,用於追蹤捲動時明顯卡頓的情況。

Quote-Fabio.jpg


在應用程式中使用 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) {
    ...
}

設定快取視窗的方法有兩種。兩者都描述相同的事物:要保留多少螢幕外內容,但單位不同。

  • Dp:固定絕對長度。無論使用何種裝置,ahead = 150.dp 都會保留超過可見邊緣 150dp 的內容。
  • 浮點值:可視區域的一部分。aheadFraction = 0.5f 會預先組成半個螢幕,因此絕對數量會隨著螢幕高度調整,支援各種板型規格:平板電腦或展開的摺疊式裝置較多,小型手機較少。

Instagram 團隊特別針對 Direct 的內容結構和項目大小,微調快取視窗的浮點分數。理想值會因特定 UI 參數而異,因此需要多方嘗試,才能找到適當的平衡點。

使用 onVisibilityChanged 記錄曝光


onVisibilityChanged API(在 Compose 1.9.0 中新增) 是 Google 與 Meta 技術合作的另一項重要成果。這項 API 可讓大型 Jetpack Compose 介面以一致的方式,得知可組合項何時實際顯示在螢幕上,取代過去使用的自訂手動實作。光是在 Instagram Direct 中,就有數百個檔案使用這些顯示信號,支援產品品質指標,而這些指標取決於 UI 元素是否實際向使用者顯示。

啟動效能

Instagram Direct 採用 Jetpack Compose 後,應用程式其他介面的效能也意外提升。Jetpack Compose 執行階段會產生暖機成本,但只需支付一次。由於訊息功能是高流量介面,使用者通常會在工作階段初期造訪,因此 Instagram 上其他依賴 Compose 的介面,效能也明顯提升。

Instagram Direct 內 Compose UI 的啟動效能,是透過基準設定檔進行最佳化,在安裝時預先編譯熱門程式碼路徑,讓 Compose 在首次啟動時就能快速算繪。

Instagram Direct 遷移至 Jetpack Compose 的經驗談 

  • Jetpack Compose 可立即帶來投資報酬:您不需要使用進階 AI 工作流程,就能從 Compose 獲益。程式碼減少約 50%,代表需要維護的程式碼變少,錯誤也減少。
  • 設計 AI 原生架構帶來了顯著成效,包括 AI 代理執行時間縮短 35%、工程師與代理之間的互動減少 32%,以及權杖成本降低 33%。
  • 雖然有許多互通 API,且支援結合 View 和 Compose,但建議您遷移較大的介面,而非個別的小型元件。這樣一來,UI 就能保持在單一不中斷的組合結構階層中,並充分發揮 Compose 原生效能最佳化的所有優勢。
  • 搭配使用可暫停組合和 LazyLayoutCacheWindow:兩者搭配使用的效果比單獨使用快取視窗更好。如果只有快取視窗,大型項目仍可能嘗試在單一階段中組成,導致超出影格預算。
  • 為 Compose 貢獻一己之力!Meta 與 Jetpack Compose 團隊合作,在 Compose 中實現他們的意見和想法。使用開放原始碼工具包,代表我們都能從集中修正錯誤和提升效能中受惠。歡迎提供意見!

Instagram 採用 Jetpack Compose 後,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 帶來效能提升!此外,我們也要感謝 Meta 的 Gary Ye 協助將 Compose 導入 Instagram Direct,以及 Meta 的 Gopal Juneja 透過資料科學支援這項工作!

繼續閱讀