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(计数):耗时 17 毫秒或更长时间渲染的帧。如果相对于启动时长而言该值较高,则说明启动期间主线程一直很忙。这可能是由繁重的布局工作、同步桥接调用或同时渲染过多组件造成的。expo.frameRate.frozenFrames(计数):耗时 700 毫秒或更长时间渲染的帧。这些是导致应用明显卡住的严重冻结。启动期间即使只有一帧也属于严重问题。通常由同步 I/O、大型 JSON 解析或主线程上的阻塞网络调用造成。expo.frameRate.totalDelay(秒):所有帧超过其目标时长的累计总时间。这是衡量“流畅度”的最佳单一指标。将其与 TTI 对比:如果 TTI 为 2.5 秒,而totalDelay为 0.1 秒,则启动速度较慢但过程流畅(时间花在了合理的工作上)。如果totalDelay为 1.5 秒,则应用在启动的大部分时间里都很卡顿,用户看到的是一个不断掉帧的屏幕。expo.device.lowPowerMode(布尔值):报告 TTI 时操作系统的省电模式(iOS 上的低电量模式,Android 上的省电模式)是否处于启用状态。省电模式会限制 CPU、GPU 和后台活动,因此如果排除此标志后 TTI 回归消失,则问题是由环境导致的,而非代码变更。expo.device.batteryLevel(数字,0–1):记录 TTI 时的电池电量比例。可用于排除在低电量时积极管理性能的设备所产生的热影响/降频影响。操作系统未报告该值时会省略。expo.device.batteryCharging(布尔值):设备是否已插入电源或正在无线充电。在 iOS 和部分 Android OEM 设备上,充电通常会提高持续 CPU 性能上限,因此与未充电样本进行比较更为保守。expo.device.thermalState(字符串):取值为nominal、fair、serious、critical、unknown。持续处于serious/critical状态会导致操作系统限制 CPU/GPU,并可能大幅减慢启动速度,与应用是否发生变更无关。expo.network.connected(布尔值):记录 TTI 时设备是否连接了可访问互联网的网络。如果只有该值为true时 TTI 才会变差,则原因可能是启动流程依赖网络;如果该值为false时 TTI 变差,则说明应用在显示缓存内容之前做了过多工作。expo.network.type(字符串):取值为wifi、cellular、ethernet、none、other、unknown。可用于比较蜂窝网络与 Wi-Fi 用户群体——两者差距较大通常说明启动工作依赖网络。VPN 流量会报告底层传输方式(通常为wifi或cellular),因为 VPN 是通过该传输方式建立隧道的。Android 和 iOS 上的取值集合刻意保持一致,因此仪表板无需按平台分别处理。expo.network.isExpensive(布尔值):操作系统是否将该连接视为按流量计费,例如蜂窝网络或个人热点。两个平台都会报告此值,且仅在存在网络连接时报告。expo.network.isConstrained(布尔值,仅 iOS):此网络路径是否启用了低数据模式。在此模式下,系统会延迟后台传输,因此等待后台传输的启动过程可能比在不受限制的网络路径上使用相同代码时更慢。expo.network.dataSaverEnabled(布尔值,仅 Android):是否启用了流量节省程序。这是 Android 上最接近低数据模式的功能,但它是进程范围的设置,而不是针对单条网络路径的设置,因此使用单独的键。
如何解读它们:
- 高 TTI + 低总延迟: 启动慢但流畅。优化阻塞启动流程的内容(bundle 大小、数据获取、初始化链)。
- 高 TTI + 高总延迟 + 很多慢帧: 主线程争用。卸载工作并简化初始渲染树。
- 高 TTI + 高延迟 + 冻结帧: 有东西在严重阻塞。检查同步 I/O、大型 JSON 解析或阻塞式 API 调用。
网络请求参数
TTI 事件还会汇总应用在启动期间发出的 HTTP 请求,让你能够区分由网络导致的启动缓慢与由应用自身工作导致的启动缓慢。统计窗口从原生启动结束开始,到调用 markInteractive() 时结束。系统会自动监测请求:iOS 上的 URLSession 流量和 Android 上的 OkHttpClient 流量,其中包括 React Native 中的 fetch。EAS Observe 不会统计其自身的遥测上传。窗口内没有请求时会省略这些参数。
expo.network.requests.count(计数):窗口内开始的请求数。expo.network.requests.failed(计数):出错或返回非 2xx 状态码的请求数。这一指标可用于判断启动是否因某个迟迟未返回的请求而停滞。expo.network.requests.bytesReceived和expo.network.requests.bytesSent(字节):窗口内的接收和发送总量,按实际传输量测量。expo.network.requests.totalDuration(秒):所有请求耗时之和,包括失败的请求。请求重叠时,该值可能超过实际经过的时间;单个超时请求会计入客户端的整个超时时间。expo.network.requests.throughputBytesPerSecond(字节/秒):数据实际传输期间的接收字节数。分母为所有传输窗口的并集,从每个请求收到第一个响应字节时开始计算,因此不包括 DNS、连接建立和服务器处理时间。缓存命中和失败的请求不计入。未接收到任何数据时会省略。expo.network.requests.slowest.*:关于已完成的最长请求的信息——duration(秒)、host(字符串)、statusCode(数字)、timeToFirstByte(秒)和bytesReceived(字节)。请结合这些参数一起查看:如果duration大部分是timeToFirstByte,说明服务器响应缓慢;如果timeToFirstByte较短但bytesReceived较大,则说明传输耗时较长。statusCode有助于解释空响应——bytesReceived为 0 在 304 响应中是正常的,在 200 响应中则是问题。
信息 汇总受限于一个内存缓冲区,该缓冲区最多保存最近 200 个请求。启动期间发出的请求超过此数量时,统计值会偏低,因此应将这些参数视为请求非常繁忙的窗口的样本,而非完整统计。
自定义事件参数
你可以通过将参数传给 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(...)。
更新下载时间
它衡量什么: 在用户设备上下载 EAS Update OTA bundle 所需的时间。当应用使用 EAS Update 和 expo-observe 时,会自动收集此指标。无需额外埋点。
有关仪表板视图、各更新的详细数据和 CLI 查询,请参见 EAS Update 下载性能。
导航指标
信息 导航指标要求使用 SDK 56 或更高版本,并启用导航集成。
启用 Expo Router 或 React Navigation 集成后,EAS Observe 会收集三个逐路由指标:
- 逐路由首次渲染(
cold_ttr):从导航操作到目标屏幕首次获得焦点所用的时间。 - 逐路由热渲染(
warm_ttr):对获得焦点前已渲染的屏幕进行相同测量,例如预加载后或返回导航时。 - 逐路由可交互时间(
tti):从导航操作到目标屏幕调用markInteractive()所用的时间。
有关设置说明以及每个指标的完整事件参数,请参见 Expo Router 或 React Navigation 集成页面。
内存警告
信息 内存警告要求使用 SDK 57 或更高版本,且仅在 iOS 上记录。
当 iOS 向应用发送低内存警告时,EAS Observe 会记录一个严重级别为 warn 的 expo.memory.warning 事件。该事件标记系统开始承受内存压力的时刻。事件会携带收到警告时的内存使用情况快照,此时应用的其他部分尚未对警告作出反应并释放内存。
内存警告表示操作系统内存不足,可能会终止应用以回收内存。如果会话在此事件发生后不久结束,很可能是由于内存耗尽而终止;此类终止不会报告为崩溃。
此事件会自动记录,无需埋点。它会显示在会话时间线和 eas observe:events 中,与用户定义的事件并列显示。
事件参数
expo.memory.allocated(字节):计入应用进程的内存占用。这是系统与应用内存限制进行比较的值,因此应重点关注此数字。expo.memory.physical(字节):常驻内存,即当前保留在物理 RAM 中的应用页面。expo.memory.available(字节):应用在达到内存限制之前还可以分配的内存。iOS 模拟器不会强制执行内存限制,因此会省略此值。expo.memory.warningsCount(计数):截至目前本次会话中记录的警告数,包括此次警告。同一会话中该计数持续上升,说明可能存在内存泄漏,或缓存始终未释放内存。
如何改进:
- 在解码大型图像前,将其缩小到实际显示尺寸。保存在内存中的全分辨率资源是内存压力的常见原因。
- 应用转入后台时,释放缓存和其他不可见资源。
- 避免将整个网络响应或文件内容保存在内存中。改用流式处理或分页处理。
- 检查是否存在保留环,以及是否有从未移除的事件监听器。
数据处理
离线收集
设备离线时收集的指标会存储在设备上。只要网络连接可用,应用转入后台时就会自动将其发送到服务器。你也可以随时调用 Observe.dispatchEvents() 手动刷新事件。
数据保留
指标数据至少保留 60 天。
采样
默认情况下,所有安装实例都会发送其指标。你可以通过设置 sampleRate,改为只从部分安装实例发送。详情请参见采样。
环境
所有指标都按环境分组,环境是附加到每个指标上的元数据标签。有关该值的来源以及如何覆盖,请参见环境。
调试构建
除非将 dispatchInDebug 设置为 true,否则从调试构建中收集的指标会在发送前被丢弃。详情请参见在开发环境中启用指标。
禁用发送
你可以使用 configure({ dispatchingEnabled: false }) 全局禁用所有发送。禁用期间,任何待发送的指标都会被丢弃而不发送;在重新将其设为 true 之前,后续指标也不会发送。