案例研究

Instagram Direct 工程师如何使用 Jetpack Compose 构建 AI 原生界面架构,并将每个代理会话的令牌成本降低了 33%

阅读用时:11 分钟

这篇博文是与 Meta 团队合作撰写的

Instagram 私信是 Instagram 的核心界面之一,每天处理数十亿条用户消息。经过多年的迭代,该团队从旧版 Android View 系统中尽可能地挤出了每一项微优化。不过,维护和扩展经过高度优化的旧版界面会产生巨大的技术债务和工程开销,尤其是在团队越来越频繁地采用声明式界面和 AI 编码助理的情况下。

为 Instagram Direct 采用 Jetpack Compose 不仅仅是典型的界面现代化。该团队构建了一个 AI 原生界面代码库,其大小比原始实现减少了 50% ,同时将 AI 智能体执行时间缩短了 35% ,将工程师与智能体之间的交互次数减少了 32% ,并将令牌成本降低了 33% 。在与 Google 的密切合作下,该团队采用了 Jetpack Compose,同时保持了较高的性能标准。通过性能优化,Meta 和 Google 不仅改进了 Instagram 的 Compose,还改进了更广泛的 Android 开发者生态系统。

大规模实现代码库现代化

AI 已迅速成为行业内工程师的日常助手,将其应用于 Instagram 等大规模代码库已带来实际的生产力提升。Instagram 私信团队设定了更具雄心的目标。该团队并未只是简单地让 AI 工具处理现有代码,而是重新设计了代码库及其架构,使其在设计上就能够充分利用 AI,从而将 AI 的影响成倍放大,远超仅通过改造所能实现的效果。

Instagram Direct 团队选择 Jetpack Compose 作为构建 AI 原生界面架构的关键组件。其声明式特性可确保代码简洁、可预测,并且在结构上更易于 AI 模型进行推理,同时减少副作用、隐式状态和更清晰的组件边界。

迁移到 Jetpack Compose 需要仔细规划。每天都有数亿人在 Instagram 上发送消息,因此迁移必须逐步进行,平稳过渡,在团队重新设计底层架构的同时,确保用户体验不受丝毫影响。为了说明挑战的规模,我们举例说明:单个界面组件可以呈现超过 160 种不同的状态排列组合,而仅一个对话界面就处理超过 200 种不同的消息类型。

Product Design 1.png

在将如此规模的代码库迁移到 Compose 时,人们很容易选择简单的方法,即将 Compose 界面组件嵌入到现有的视图层次结构中。作为逐步迁移过程中的一个增量步骤,这完全有效。不过,从长远来看,在基于 View 的代码库中集成 Compose 会带来挑战。AI 工具通常会选择最省力的方式。如果您混合使用声明式和命令式界面代码,AI 很可能会错误地混合使用它们,从而引入细微的 bug、技术债务和性能回退。

构建 AI 原生界面架构

在 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 重新绑定和回收过程中会保留下来,从而导致难以重现的内存泄漏和 bug。

即使通过为商品提供专用 @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 技能弥补设计缺陷的做法无法扩缩,因为加载到上下文中的每个技能都会消耗 token,并可能会降低智能体的性能。相反,架构本身应承担这一重任。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 的数百个界面组件必须与其旧版对应组件共存,并且两者并行维护。AI 工作流加快了编写大量代码的流程,从而实现了这种并行迁移。正是这种方法让 Direct 团队在创纪录的时间内完成了迁移,而且没有打扰到其他团队成员,他们继续交付着每天都在改善数百万人体验的功能。

多位工程师针对迁移期间构建的可重复使用的技能和惯例共享知识库运行了自己的 AI 代理。这样一来,整个团队的工作流程和最佳实践就能保持同步,而无需每位工程师重新探索。在每个平台中,该团队按以下阶段执行了迁移:

  • 使用 AI 编写所有 Compose 代码。
  • 不断完善,处理极端情况并弥补性能差距,直到在公开测试中向真实用户推出该界面。

将工作分为两个阶段(每个屏幕一个阶段),这样一位工程师就可以快速完成整个界面,提前确定架构并处理棘手的边缘情况。有了这些基础工作,其他人就可以专注于让界面达到可用于生产用途的水平,而无需停下来自行做出这些技术决策,从而保持整体迁移速度。

迁移结果验证了该方法。对于迁移的 Instagram Direct 界面,Jetpack Compose 使团队能够将界面代码总量减少 50%。需要 AI 生成的代码越少,输出质量越高,每项任务的 token 成本越低。

Quote-Pavlo-New.jpg

对 Instagram Direct 的 Android 代码库进行内部数据分析,比较了使用 Compose 界面处理相同任务的 AI 智能体会议与使用 Android View 处理相同任务的 AI 智能体会议。在以下两个方面,效率提升效果非常明显:

  • 每个字符的已着陆代码:所需的工程师-代理交换次数减少了 32% ,代理执行时间减少了 35% (从代理开始处理工程师的请求到返回响应所经过的时间)。
  • 每次代理会话:与“查看”相比,“撰写”功能使令牌费用总共降低了 33% 。

我们会同时报告输出效率和典型会话数,因为它们都是有用的独立结果。工程师代理的交换次数和执行时间数据比较的是每个已完成输出单元的资源使用情况,而 token 数据比较的是典型代理会话的总费用。

数据还显示,这两个框架在处理复杂或脆弱的代码时存在一致的差异。Meta 会使用代码更改的风险得分来跟踪此指标,该得分会评估总体代码质量以及更改导致生产事件的可能性。该分析通过 token 消耗量、代理的执行时间和工程师与代理的互动次数的组合来衡量代理的资源效率。随着文件累积的风险得分越来越高,AI 智能体会议自然会变得越来越低效。

当文件的累积风险得分翻倍时,使用 Android 视图实现的界面会使代理资源效率降低 30%(按着陆字符数计算)。在相同情况下, Jetpack Compose 界面仅减少了 9%

通过 Google 和 Meta 之间的合作,Instagram Direct 团队为采用 Compose 带来了新的视角,他们从代码库的 AI 就绪程度(而不仅仅是界面重写)的角度来考虑采用 Compose。这项工作表明,Compose 在作为构建 AI 优先代码库和架构的基础方面具有强大的优势,尤其是在应用于 Instagram 等规模的应用时。

性能优化 

Instagram 私信是该应用最重要的界面之一,用户希望它始终能提供快速响应的体验。采用 Jetpack Compose 实际上意味着要大幅重写界面,而首要目标是保持高质量的体验,不出现任何回归问题。

经过多年的迭代,Instagram 上基于 View 的旧版实现已达到极高的性能标准,而团队在迁移到全新的界面框架时,需要达到相同的标准。

Instagram 会衡量数百甚至数千个效果指标。对于 Compose 采用,以下三点最为重要:

  • 可互动时间 - 从打开屏幕到能够使用屏幕之间的时间。
  • 完全加载时间  - 从打开屏幕到所有内容(即图片)完全加载完毕之间的时间。
  • 滚动性能 - 屏幕滚动有多流畅,是否会丢帧。

这些指标在生产环境中运行时进行跟踪,因此可以运行 A/B 测试,将迁移后的 Compose 界面与旧版界面进行比较,并评估此项工作对性能的影响。

处理此类迁移的常见方法是从小处着手,先迁移少量界面组件,然后收集数据并研究它们的行为。虽然这些早期结果很有帮助,但它们只反映了部分情况,可能会因以下原因而错误地否定采用 Compose:

  • 不具代表性 - 迁移的单个界面组件可以提供有关其在特定屏幕上的整体性能的有用数据。不过,不同组件的行为方式各不相同,原因也各不相同,因此您无法总是从中进行外推。
  • 互操作费用 - 在大型视图代码库中使用少量 Compose 代码时,两个系统之间的桥接费用不可预测。这种开销会扭曲测量结果,因此早期的小规模结果无法反映完全迁移的实际情况。

因此,虽然小型迁移很有用,但并不总是能反映出 Compose 的全部影响。 迁移的表面越多,端到端迁移的次数越多,没有桥接中断,图片在性能方面就越清晰、越好。

Instagram Direct 中的核心界面围绕各种类型的长列表构建,最初使用 RecyclerView 实现。该架构依赖于自定义抽象来实现可伸缩性,但仍受基于视图的系统的生命周期限制。

Diagram 1.png

该团队的主要任务是在现有的基于 RecyclerView 的架构中将数百个单独的列表项逐步迁移到 Compose,并在 A/B 测试下以小规模独立群组的形式在生产环境中推出这些列表项,而所有这些操作都不会对用户的消息体验产生明显变化。

这种设置的最大缺点是,即使在将每个列表项完全迁移到 Compose 后,仍然会通过核心 RecyclerView 架构严重依赖旧版 View 系统。作为自然而然的下一步,该团队决定投资于将基于 RecyclerView 的核心架构替换为原生 Compose 替代方案 LazyColumn。

这意味着,Compose 界面组件应从其封装的框架中抽象出来,同时仍与 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 中默认启用)允许跨帧逐步组合开销较大的延迟列表项,以防止出现卡顿。与 Compose 1.9 中添加的 LazyLayoutCacheWindow 搭配使用时,可显著提升滚动流畅度。在 Meta 最近的内部测试中,与纯 Compose 相比,将可暂停的组合与单视口 LazyLayoutCacheWindow 相结合,每分钟的大幅掉帧数 (LFDs/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 会在可见边缘之外保留 150 dp 的已组合内容,无论设备如何。
  • 浮点数:视口的分数。aheadFraction = 0.5f 会预先合成半个屏幕,因此绝对数量会随屏幕高度而变化,从而支持各种外形规格:在平板电脑或展开的可折叠设备上更多,在紧凑型手机上更少。

Instagram 团队专门针对 Direct 的内容结构和项目大小,对缓存窗口的浮点数分数进行了微调。由于理想值因具体的界面参数而异,因此需要进行一些实验才能找到合适的平衡点。

使用 onVisibilityChanged 进行展示日志记录


onVisibilityChanged(在 Compose 1.9.0 中添加)API 是 Google 与 Meta 之间技术合作伙伴关系的另一项重要成果。它为大规模 Jetpack Compose Surface 提供了一种一致的方式来了解可组合项何时实际显示在屏幕上,取代了过去使用的自定义的手动实现。仅在 Instagram Direct 中,这些可见性信号就用于数百个文件,以支持取决于界面元素是否实际向用户展示的产品质量指标。

启动性能

Instagram Direct 采用 Jetpack Compose 后,应用的其他界面也获得了意想不到的性能提升。Jetpack Compose 运行时会产生预热成本,但您只需支付一次,而且由于消息传递是用户会话中经常访问的高流量界面,因此依赖 Compose 的 Instagram 其他界面也获得了明显的性能提升。

通过使用基准配置文件,优化了 Instagram Direct 中 Compose 界面的启动性能,该配置文件会在安装时预编译热门代码路径,以便 Compose 从首次启动开始就能快速渲染。

从 Instagram Direct 迁移到 Jetpack Compose 的经验 

  • Jetpack Compose 可立即带来投资回报:您无需使用高级 AI 工作流即可受益于 Compose。代码量减少约 50%,这意味着需要维护的代码更少,bug 暴露面也更小。
  • 设计 AI 原生架构带来了显著优势,包括将 AI 智能体执行时间缩短了 35%、将工程师与智能体之间的交流次数减少了 32%,以及将令牌成本降低了 33%。
  • 虽然有许多互操作 API 和对组合使用 View 和 Compose 的支持,但应争取将较大的界面迁移到 Compose,而不是单个小型组件。这样可确保界面位于单个不间断的组合层次结构中,并充分利用所有最佳的 Compose 原生性能优化。
  • 将可暂停的组合与 LazyLayoutCacheWindow 配对:将这两者配对在一起比单独使用缓存窗口效果更好。如果只有缓存窗口,较重的项仍可能会尝试在单次传递中进行合成,从而可能超出帧预算。
  • 为 Compose 本身做出贡献!Meta 与 Jetpack Compose 团队合作,在 Compose 中实现他们的反馈和想法。使用开源工具包意味着,当集中进行 bug 修复和性能改进时,我们所有人都会受益。因此,请务必告知我们您的反馈!

采用 Jetpack Compose 后,Instagram 在 AI 辅助开发方面取得了显著进步,同时简化了日常界面工程。声明式方法可减少样板代码,使状态更易于推理,并提高开发者的整体工作效率。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 通过数据科学支持这项工作!

继续阅读