This documentation is available as Markdown for AI agents and LLMs. See the full Markdown index or append .md to any documentation URL.
指标参考
编辑页面
EAS Observe 跟踪的每个性能指标的参考,包括概念和数据处理。
参考 EAS Observe 收集的性能指标、用于组织事件的核心概念(会话和用户),以及所收集数据的保留方式。
概念
会话
会话在应用进程启动时开始,在应用进程终止时结束。每个会话都有一个唯一标识符,并包含该次应用启动期间收集的所有指标。
用户
用户由一个匿名 ID 标识,该 ID 对每个应用安装实例都是唯一的。此 ID:
- 在应用首次安装时生成
- 在应用更新之间保持不变
- 如果用户卸载并重新安装应用,则会重置
- 不是个人身份信息(PII)
这使你能够在不收集个人数据的情况下,查看同一用户跨多个会话的指标。
指标
信息 所有时长指标均以秒为单位报告。
冷启动时间
它衡量什么: 从进程创建到系统完成内存分配、启动一个全新的运行时环境、从磁盘加载应用代码和资源,以及在渲染 UI 之前初始化其组件所花费的时间。这是最慢的一种启动类型,通常发生在全新安装、应用升级、设备重启之后,或操作系统为回收内存而终止应用时。它是仅原生端的指标,这意味着你的 JavaScript 代码不会影响该指标,但会包含 React Native 运行时初始化。
该指标会自动收集。
如何改进:
- 移除未使用的原生模块。
- 避免静态初始化器(Objective-C 中的
+load方法、C++ 中的静态构造函数),以及会添加它们的原生模块或配置插件。 - 保持应用的内存和 CPU 使用率较低,以便操作系统在后台时不会终止进程。这不会影响该指标本身,但会使后续启动变为热启动而不是冷启动。
- 如果你使用带有非零
fallbackToCacheTimeout的expo-updates,应用启动会因等待更新检查而被阻塞。请将该值保持为0(默认值),或将checkOnLaunch设置为NEVER或ERROR_RECOVERY_ONLY,以避免延迟冷启动。
我们的建议: 低于 1.5 秒。
热启动时间
它衡量什么: 当操作系统已经将应用进程保留在内存中,只需要将其带回前台并重建视图层次结构时,就会发生热启动。与冷启动不同,大多数原生资源和服务已经在内存中,因此这种启动类型明显更快。
应用无法自行预热:操作系统根据系统压力和最近使用情况来决定哪些进程保留在内存中。你可以影响热启动的持续时间,但不能决定是否会发生热启动。
该指标会自动收集。
如何改进:
- 移除未使用的原生模块。
- 减少视图层次结构中的视图数量。操作系统在热启动时必须重建视图树,因此过深或过于臃肿的树恢复起来会更慢。
我们的建议: 低于 0.5 秒。
Bundle 加载时间
它衡量什么: 加载 JavaScript 字节码并对其求值的持续时间。它从 bundle 开始加载时开始,到 bundle 完成求值时结束,并且发生在调用 runApplication 之前。
该指标会自动收集。
如何改进:
- 减少 bundle 大小:
- 使用 tree shaking(从 Expo SDK 54 起默认启用),并遵循有助于 Metro 剥离不必要代码的规则。参见 Tree shaking 和代码移除。
- 分析你的 JavaScript bundle,移除未使用和体积较大的依赖项。参见 使用 Expo Atlas 分析 JavaScript bundle。
- 使用
React.lazy()对大型屏幕和组件进行懒加载。参见 优化 JavaScript 加载。 - 避免在顶层作用域阻塞 JavaScript 线程:
- 不要进行重型计算。
- 推迟任何同步 I/O 操作(存储读写)。
我们的建议: 低于 0.3 秒。
首次渲染时间(TTR)
它衡量什么: 从应用完成原生启动到根 React 组件首次在屏幕上渲染的时间。这是 React 实际渲染出内容的时刻,发生在闪屏隐藏之后。每个应用的目标都应该是尽可能快地显示出有意义的内容,即使那只是一个骨架屏加载界面。
当你使用 root HOC 包裹根布局时,此指标会自动收集(参见 开始使用):
如何改进:
- 减少 bundle 加载时间(见上文)。
- 避免同步 I/O 操作(存储读写)。
- 避免阻塞网络请求。
- 保持初始渲染树尽可能小(延后重组件)。
- 使用轻量级屏幕作为初始路由。
- 尽量减少会阻塞渲染的
useEffect和useLayoutEffect链。
我们的建议: 包括冷启动时间在内低于 2 秒。
可交互时间(TTI)
它衡量什么: 从热/冷启动开始,到用户实际上可以点击、滚动并以其他方式与应用交互的时间。这是最重要的启动指标,因为它反映了用户感知中的“应用已准备就绪”。
该指标不会自动报告。要开始衡量它,请在屏幕准备好供用户交互时调用 markInteractive(),例如在初始数据加载完成后的 useEffect 中。如果你的应用使用深度链接,主屏幕可能并不总是初始屏幕,因此我们也建议在其他屏幕上调用此函数。每次启动只记录第一次调用,因此可以安全地多次调用它(例如,用户在屏幕之间导航时)。如果你使用 expo-router,该指标的事件会自动包含当前路由名称。
在组件内的 useObserve() hook 中调用 markInteractive()。
从应用中的任意位置调用 AppMetrics.markInteractive()。
什么算作“可交互”?
以下条件都必须成立:
- 内容已渲染到屏幕上(不仅仅是闪屏或骨架屏)。
- 触摸处理函数已绑定且响应正常。
- 导航可用。
如何提高测量准确性: 仅在屏幕内容已加载且触摸处理函数已激活后调用 markInteractive(),而不是只在组件挂载时调用。如果你的屏幕在可用之前需要先获取数据,请在数据准备好之后再调用。
如何改进:
- 减少首次渲染时间(见上文)。
- 避免在显示可交互内容之前发生瀑布式数据获取。
- 优化初始网络请求。
- 避免渲染大型列表(使用 FlashList 或 LegendList)。
- 减少可能阻塞 JavaScript 线程和交互的重型工作(I/O 操作、状态恢复、JSON 解析)。
- 如果可能,先显示缓存或本地数据。
我们的建议: 包括冷启动时间在内低于 3 秒。
自动事件参数
每个 TTI 事件都包含额外参数,以帮助排查问题:
expo.frameRate.slowFrames(count): 渲染耗时 17ms 或更长的帧数。如果该值相对于启动时长很高,则说明主线程在启动期间持续繁忙。指向繁重的布局工作、同步桥接调用,或一次渲染过多组件。expo.frameRate.frozenFrames(count): 渲染耗时 700ms 或更长的帧数。这些是严重冻结,应用会明显卡住。即使启动期间只出现一次,也说明问题非常严重。通常由同步 I/O、大型 JSON 解析,或主线程上的阻塞式网络调用引起。expo.frameRate.totalDelay(seconds): 所有帧超出目标时长的累计总时间。这是单一最佳的“流畅度”指标。将其与 TTI 对比:如果 TTI 是 2.5s,而totalDelay是 0.1s,那么启动慢但流畅(时间花在了合理的工作上)。如果totalDelay是 1.5s,那么应用在大部分启动过程中都很卡顿,用户一直盯着一个抖动的屏幕。expo.device.lowPowerMode(boolean): 报告 TTI 时操作系统省电模式(iOS 上的低电量模式、Android 上的省电模式)是否处于开启状态。省电模式会限制 CPU、GPU 和后台活动,因此如果过滤掉该标志后 TTI 回归消失,则原因是环境因素而不是代码变更。expo.device.batteryLevel(number, 0–1): TTI 时的电池电量比例。对于在低电量时会积极管理性能的设备,排查热/限频影响很有用。如果操作系统不报告该值,则会省略。expo.device.batteryCharging(boolean): 设备是否接入电源或正在无线充电。充电通常会提高 iOS 和某些 Android OEM 的持续 CPU 性能上限,因此未充电样本更适合作为保守比较对象。expo.device.thermalState(string):nominal、fair、serious、critical、unknown之一。持续处于serious/critical状态会导致操作系统限制 CPU/GPU,并可能在不依赖任何应用变更的情况下显著减慢启动。expo.network.connected(boolean): TTI 时设备是否具有可访问互联网的网络。如果 TTI 仅在该值为true时变差,则原因很可能是受网络限制的启动路径;如果在false时变差,则说明应用在显示缓存内容之前做了过多工作。expo.network.type(string):wifi、cellular、ethernet、none、other、unknown之一。用于比较蜂窝网络与 Wi-Fi 人群——较大的差距通常指向受网络限制的启动工作。VPN 流量会按其底层传输方式报告(通常是wifi或cellular),因为 VPN 是在其之上隧穿的。该取值集合在 Android 和 iOS 上保持一致,因此仪表盘不需要按平台分支。
如何解读它们:
- 高 TTI + 低总延迟: 启动慢但流畅。优化阻塞启动流程的内容(bundle 大小、数据获取、初始化链)。
- 高 TTI + 高总延迟 + 很多慢帧: 主线程争用。卸载工作并简化初始渲染树。
- 高 TTI + 高延迟 + 冻结帧: 有东西在严重阻塞。检查同步 I/O、大型 JSON 解析或阻塞式 API 调用。
自定义事件参数
你可以通过将参数传给 markInteractive(),为 TTI 事件附加自己的参数。这对于按应用特定维度切分 TTI 很有用,例如用户群组、租户、功能标志变体,或屏幕加载的内容类型。
import { useObserve } from 'expo-observe'; const { markInteractive } = useObserve(); markInteractive({ params: { tenant: 'acme', cohort: 'beta', cacheHit: true, }, });
import { AppMetrics } from 'expo-observe'; AppMetrics.markInteractive({ params: { tenant: 'acme', cohort: 'beta', cacheHit: true, }, });
你还可以覆盖附加到事件上的路由名称;否则该名称会从 Expo Router 检测到的初始路由中填充。当屏幕的逻辑名称与路由路径不同、路由是动态路由,或者未使用 Expo Router 时,这一点很有用:
import { useObserve } from 'expo-observe'; const { markInteractive } = useObserve(); markInteractive({ routeName: '/feed', params: { cacheHit: true }, });
import { AppMetrics } from 'expo-observe'; AppMetrics.markInteractive({ routeName: '/feed', params: { cacheHit: true }, });
参数值可以是字符串、数字、布尔值或其他可 JSON 序列化的值。
以声明式方式标记可交互
你无需从 effect 中调用 markInteractive(),而是在屏幕变为可交互的那个位置渲染 <ObserveInteractiveMarker /> 组件,例如在初始数据加载完成后。它会在挂载时调用一次 markInteractive(),并且不渲染任何内容。
信息
ObserveInteractiveMarker在 SDK 56 及以后版本中可用。
import { ObserveInteractiveMarker } from 'expo-observe'; function Feed({ items }) { if (!items) { return <Spinner />; } return ( <> <FeedList items={items} /> <ObserveInteractiveMarker /> </> ); }
该标记器只会在挂载时触发一次,因此其 params 取自首次渲染。之后修改它们不会产生任何效果,并且会在开发环境中记录警告。如果你需要附加那些只会在稍后才知道的参数,请改为直接调用 useObserve().markInteractive(...)。
数据处理
离线收集
在设备离线时收集的指标会保存在设备上。只要应用切到后台时设备有网络连接,它们就会自动发送到服务器。你也可以随时调用 Observe.dispatchEvents() 手动刷新事件。
数据保留
指标数据至少保留 60 天。
采样
默认情况下,所有安装实例都会发送其指标。若要改为仅从一部分安装实例发送,请在调用 configure() 时设置 sampleRate。该决策对每个安装实例都是确定性的,因此同一安装实例在应用启动之间始终会处于采样内或采样外。
环境
所有指标按环境分组。默认情况下,环境值由 process.env.NODE_ENV 派生(如果未设置,则回退为 'production'),也可以通过 configure({ environment }) 覆盖。环境是附加到每个指标上的元数据标签,不会影响指标是否被发送。
调试构建
除非通过 configure() 将 dispatchInDebug 设为 true(更多信息参见 在开发中启用指标),否则从调试构建中收集的指标会在发送前被丢弃。如果原生应用是调试构建,或者 JS bundle 是开发 bundle(__DEV__ 为 true),则会被视为调试构建。此检测与 environment 值无关。
禁用发送
你可以使用 configure({ dispatchingEnabled: false }) 全局禁用所有发送。在禁用期间,任何待发送的指标都会在不发送的情况下被丢弃,并且在重新设为 true 之前不会再发送任何后续指标。