推出 Observe:面向 Expo 应用的性能监控,现已正式发布

Observe 已正式可用。React Native 的启动性能与单屏性能,在真机上测量,并与每次构建和更新关联。

中文
复制
hero-image

*你的应用在生产环境里到底跑得怎么样?*这个问题出奇地难回答。崩溃上报工具会在应用挂掉时告诉你,但它悄悄变慢的时候不会。而如果你最近发过几个原生构建和几次 JavaScript 更新,想弄清是哪一次导致了性能退化,基本只能靠猜。

这正是 Observe 要解决的问题。它测量你的应用在真实用户设备上的启动速度,以及每个界面变得可用的速度,并在图表上为你发布的每一个原生构建和每一次 EAS Update 打上标记。点击标记,就能看到版本号、构建号或更新 ID,以及该点对应的指标值。

Observe 从 5 月起一直处于公开测试阶段,从今天起正式可用,支持按路由划分的指标、会话时间线、agent 交接流程,以及从免费起步的按用量计费。

可观测性平台由两部分组成:

  • 开源库 expo-observe,按标准的 OpenTelemetry 规范采集指标、事件和日志,并发送到 collector。

  • EAS Observe 是负责存储和分析这些数据的服务。它是 expo-observe 的默认 collector,但你也可以配置 endpoint 使用自己的 collector。用自己的 collector 唯一会失去的是发布归属:把某项指标与产生它的 EAS 构建和 commit 关联起来,需要构建流水线。

主仪表盘

为什么移动端监控和 Web 监控不是一回事

在 Web 上,你发布一个版本,所有人下一次加载页面时跑的就是它。移动端不是这样:用户按自己的节奏更新,有些人干脆永远不更新,而一旦你在原生构建之上再叠加 JavaScript 热更新,组合数量就会成倍增长。

三个原生版本加两次更新,就意味着生产环境里同时有五个活跃版本。每一个实际上都是性能特征各不相同的独立应用,跑在数百款机型、横跨十年的操作系统版本上。

如果监控工具把「发布」建模成一个版本字符串,它就描述不了这种局面。它会若无其事地给你一个把五个不同应用平均到一起的 p90。

启动指标,无需任何埋点

装上库,包一层根布局,三个启动指标就开始上报了:

  • 启动耗时,从进程创建到系统完成内存分配,分为冷启动和热启动

  • Bundle 加载,加载 JavaScript 字节码并执行

  • 渲染耗时(TTR),从原生启动结束到你的根 React 组件首次渲染

npx expo install expo-observe
import { ObserveRoot } from 'expo-observe';

function RootLayout() {
  return <Stack />;
}

export default ObserveRoot.wrap(RootLayout);
单指标

Time to interactive(TTI) 是唯一一个需要你写一行代码的指标。没有任何库能检测出你的应用何时真正准备好接受输入:只有你的代码知道启动屏的工作何时结束、首个界面何时拿到了真实数据。所以这个调用得你自己来:

import { useObserve } from 'expo-observe';

export default function HomeScreen() {
  const { markInteractive } = useObserve();

  useEffect(() => {
    if (data) markInteractive();
  }, [data]);

  // ...
}

每个指标我们都会给出中位数、平均值、最小值、最大值、p90 和 p99。先拿中位数和上一个版本比,再按需深入最慢的那些事件。

每个会话都带设备上下文

每个 TTI 事件都会自动携带上下文:应用版本和构建号、环境、国家、操作系统及系统版本、设备型号、Expo 版本、React Native 版本、语言标签、应用标识符和路由。

还有一组字段是源自 Web 的工具采集不到的,因为这些东西在浏览器标签页里根本不存在:

  • 冻结帧、慢帧和总延迟

  • 低电量模式

  • 热状态

  • 网络类型以及网络是否已连接

p99 TTI 为四秒,在一台处于热降频、走蜂窝网络的中端 Android 设备上是一回事,在一台连着 wifi 的新 iPhone 上完全是另一回事。没有这些字段,你看到的就是一个没有解释的数字。

每次构建和更新的发布标记

每个原生构建和你发布的每次更新,都会在首个事件到达时于图表上打下一个发布标记。每张启动卡片还会把最新版本和上一个版本并排显示,无需任何筛选。你也可以把整个仪表盘筛选到某一个应用版本、某一个原生构建或某一次具体更新。

版本对比

同一个应用版本不等于同一个构建,Observe 会把它们区分开。拿我自己数据里的一个例子来说:1.0.1 这个版本有两个不同的构建,其中一个只有一次安装。显然是个测试构建,而任何把版本号当作发布版本的统计工具都看不到它。

这种方式能抓到的一类故障是:某次 JavaScript 更新让渲染变慢了。它不会崩溃,所以崩溃上报工具一片安静;它也不走应用审核,所以外部没有任何东西会标记它。在没有 Observe 之前,你只能等用户来抱怨。现在,更新发出的那一刻图表上就有了标记,曲线紧接着就抬升。

更新下载时间有单独的标签页,里面按更新列出表格,显示下载次数、中位数和 p90。通常就是在这里你会发现,一个 4MB 的资源会让蜂窝网络下的用户多等好几秒才能用上你的应用。

详情见 Builds and updates 文档

按路由统计的指标

启动指标只能反映你的第一个界面。在 SDK 56 及更高版本中,启用 Expo Router 或 React Navigation 集成后,会自动为每个路由加上冷启动和热启动的首次渲染时间;在这些界面上添加 markInteractive() 调用后,还会加上每个路由的交互就绪时间。

import { Observe } from 'expo-observe';

Observe.configure({
  integrations: { 'expo-router': true },
});

首次渲染和后续渲染分开统计,这很实用,因为第一次进入某个界面和第五次进入,通常是两种不同的性能问题。

同一时间线上的自定义事件

Observe.logEvent() 可以发出带任意可序列化属性的事件,它会和启动指标落在同一条会话时间线上。

import { Observe } from 'expo-observe';

Observe.logEvent('payment_failed', { reason: 'card_declined', attempt: 2 });

真正有用的是单个用户的完整时间线:bundle 加载、首次渲染、交互就绪、冷启动,然后是 payment_failed / reason: card_declined,接着是一次更新下载,全部按顺序排列,并附带完整的设备上下文。

错误上报(预览)

在 SDK 57 及更高版本中,expo-observe 还会记录 JavaScript 错误:未处理的错误自动记录,渲染错误通过 ObserveErrorBoundary 记录,已处理的错误通过 Observe.reportError 记录。它们会出现在同一个构建的性能指标旁边的 Errors 标签页里,所以让你 TTR 上升的那个版本和引入错误的那个版本,看的是同一个视图。

Expo Observe 中的错误报告

生产构建的堆栈跟踪指向的是压缩后的 bundle,想让它变得可读,就要在 eas.json 的构建 profile 里设置 uploadSourceMaps: true。EAS Build 会为每次构建上传 source map,该构建产生的错误会自动符号化。错误上报目前处于预览阶段,EAS Update 的 source map 和原生崩溃上报还在路上。

顺带一提:如果你在用 Sentry 或其他崩溃上报工具,继续用就好。Observe 还抓不到原生崩溃(暂时!),两者并行不冲突。

把回归问题交给 agent

发现异常是一回事,搞懂它是另一回事。p90 TTI 出现尖峰,你还是得手动交叉比对设备、版本、国家和构建。

Dashboard 上有一个「Hand off to your AI assistant」按钮。它会把当前 dashboard 的状态复制成一段写好的 prompt,里面教 agent 几条 CLI 命令,你可以直接粘进 Claude Code、Cursor 或 Codex,问它上一个 release 为什么回归了。

如果你连配置也想让 agent 来做,expo.dev/expo-skills 上发布了三个 skill:expo-observe-setupexpo-observe-metricsexpo-observe-queries

限制

Observe 已经正式可用,但不代表它做完了。以下是它目前还做不到的事:

  • 还没有告警。 现在你只能自己去 dashboard 看,或者用 eas-cli 定期自动检查指标。邮件、Slack 和 webhook 告警正在做。

  • 没有 session replay、产品分析或后端 tracing。 Observe 衡量的是 app 性能。它不是 APM,也不会跟着请求走进你的服务。

  • 还没有原生崩溃上报。 JavaScript 错误上报在 SDK 57 及以后版本处于预览阶段;原生崩溃仍然需要 Sentry 这类服务。

  • 只支持 iOS、Android 和 tvOS,且不能在 Expo Go 里运行。 你需要 development build 或 production build。

  • 需要 Expo SDK 55 或更高版本,并且有 EAS 项目。

  • 开启 Observe 需要重新构建二进制。 目前没法直接从 eas.json 打开。

  • 用户按安装维度匿名。 你能找到出问题的那次会话,但无法把它对应到某个具名客户。

  • 帧数据依附于 TTI 事件。 某个屏幕上没有 markInteractive() 调用,就没有对应的帧数据。

  • 采样不会外推。 如果调低 sampleRate,百分位数是在你保留的那部分数据上计算的,而不是重新加权推算出来的。

  • 仅限美国托管。 暂不提供欧盟数据驻留。

定价

定价按事件用量计费,仪表盘会显示你的预估用量,账单不会让你措手不及。

Free 套餐每月包含 10 万条事件,并提供启动指标以及构建和更新覆盖层。Starter 套餐(19 美元/月)和 Production 套餐(199 美元/月)均包含 50 万条事件,超出部分按用量计费。额外事件为每百万条 5.00 美元,用量更大时降至 4.75 美元,再降至 4.50 美元。

指标数据至少保留 90 天。完整细节见定价页面

从 Observe 开始

视频:如何将 Observe 添加到你的项目——在 expo.dev 上观看

按照入门指南操作,或者让 agent 用 expo-observe-setup skill 替你完成。重新构建你的应用,指标就会开始出现在你的项目仪表盘Observe 标签页中。

交给 AI 接手

问题和 bug 报告请发到 Expo Discord 的 #eas 频道,或者带到我们本周的直播里来!

来源: Expo Blog← 返回首页