Skip to content

RN 运行时按需测速

Mpx 跨端输出 React Native 时,运行时核心组件(mpx-view / mpx-text / mpx-simple-view / mpx-simple-text 等)以及 useTransformStyle / __getStyle 等公共函数是高频热路径。Hermes Profiler / Flipper 难以直接表达 Mpx 自身的逻辑分段,@mpxjs/perf 提供可按构建关闭、关闭态零残留的显式性能探针。

三类统计能力

统计类型API输出典型用途
区段聚类统计aggrStart / aggrEndMap<string, AggrResult>高频 render、hook、函数的 count/sum/avg/max
区段序列统计traceStart / traceEndTraceTimeline模块火焰图、嵌套调用 profile、异步区段瀑布图
点序列统计markMarkTimeline数据就绪、首次渲染、页面可交互等里程碑

start/end 控制整个录制窗口,Reporter 在窗口结束时同步收到三类结果。聚类统计不保留逐次样本,适合高频热路径;trace 和 mark 会保留事件对象,只适合有限插桩点和诊断窗口。

设计原则

@mpxjs/perf 采用「编译期常量开关 + 运行时探针实现 + tree-shaking 兜底」三层结构:

  1. MpxWebpackPlugin 通过 DefinePlugin 注入 __mpx_perf__ 和分组常量。
  2. 探针调用直接包在 if (__mpx_perf_framework__) / if (__mpx_perf_user__) 字面量条件里。
  3. Terser 消除关闭分组的调用点,webpack tree-shaking 继续剔除失活的实现模块。

关闭态产物中不会保留探针实现、名称字符串或模块依赖。

该方案不支持线上动态开关

线上开关意味着探针字节必须进入产物,与关闭态零残留目标冲突。线上诊断需重新构建一个开启探针的内测包。

配置入口

mpx.config.jspluginOptions.mpx.plugin 下配置 perf

js
const { defineConfig } = require('@vue/cli-service')

module.exports = defineConfig({
  pluginOptions: {
    mpx: {
      plugin: {
        perf: {
          enable: !!process.env.MPX_PERF,
          probes: ['framework', 'user']
        }
      }
    }
  }
})

配置项说明

字段类型说明
enableboolean总开关。false 或不传时整套探针关闭。
probesstring[]当前支持 'framework' / 'user'。空数组等价于关闭,未知分组会在编译期报错。
分组控制对象典型用途
framework框架内建探针,如 view:render:* / getStyle:*调试 Mpx 框架自身渲染性能
user业务自定义探针定位业务流程和函数耗时

两个分组独立 DCE,但共享同一个录制窗口和 Reporter。

录制窗口

ts
import { start, end } from '@mpxjs/perf'

router.beforeEnter('/goods/:id', () => {
  if (__mpx_perf__) {
    start({
      markLimit: 2048,
      traceLimit: 4096
    })
  }
})

router.beforeLeave('/goods/:id', () => {
  if (__mpx_perf__) end()
})
ts
interface PerfStartOptions {
  markLimit?: number
  traceLimit?: number
}
  • markLimitMarkTimeline 的总事件上限,包含 start/end 边界,默认 1024,必须是不小于 2 的整数。
  • traceLimit 是 trace 区段上限,默认 1024,必须是非负整数;传 0 可关闭当前窗口的 trace 存储。
  • 无效值回退为 1024。
  • 录制中重复调用 start(options) 保持幂等,不清空数据,也不修改当前容量。
  • 新窗口重新读取配置,不继承上一窗口的自定义容量。

start() 自动生成 { name: 'start', start: 0, timestamp: startedAt }end() 自动生成同结构的 end 边界。即使没有显式探针,完整窗口也会触发 Reporter。

区段聚类统计

聚类 API 将同名样本实时合并为 { count, sum, avg, max },不保存逐次耗时。

id 模式

默认 id 模式支持嵌套、乱序结束和同名并发,也是同步高频路径的首选:

ts
import { aggrStart, aggrEnd } from '@mpxjs/perf'

function expensiveCompute (data) {
  let id = -1
  if (__mpx_perf_user__) id = aggrStart('myBiz:list:filter')
  const result = data.filter(/* ... */).sort(/* ... */)
  if (__mpx_perf_user__) aggrEnd(id)
  return result
}

未录制时 aggrStart(name) 返回 -1 且不读取时钟。录制中使用数组槽位和 free list,稳态零对象、零闭包分配。

name 模式

开始和结束不方便传递 id 时,显式传入 useName: true

ts
if (__mpx_perf_user__) aggrStart('goods:request', true)

loadPageData().finally(() => {
  if (__mpx_perf_user__) aggrEnd('goods:request')
})

name 模式允许在录制窗口外保存起点,只有 aggrEnd(name) 发生在录制中时样本才会进入结果。后一次同名 start 会覆盖前一次起点,同名并发必须改用 id 模式。

区段序列统计

trace 保存每次区段的开始位置和持续时长,可还原模块火焰图或异步瀑布图:

ts
import { traceStart, traceEnd } from '@mpxjs/perf'

let appId = -1
let routerId = -1
if (__mpx_perf_user__) {
  appId = traceStart('module:app')
  routerId = traceStart('module:router')
}

if (__mpx_perf_user__) {
  traceEnd(routerId, { moduleId: 42 })
  traceEnd(appId, { moduleId: 1 })
}

trace 在 start 时预留事件位置,在 end 时回填 duration / info,因此嵌套区段始终按 start 顺序输出。未完成区段在窗口结束时从 events 移除,并计入 incomplete

跨作用域且不方便传递 id 时可以使用 name 模式:

ts
if (__mpx_perf_user__) traceStart('request:goods', true)

loadPageData().finally(() => {
  if (__mpx_perf_user__) {
    traceEnd('request:goods', { status: 'fulfilled' })
  }
})

trace 必须在录制窗口中 start。name 模式同名覆盖时,被覆盖的旧区段会计入 incomplete;同名并发、递归和严格嵌套必须使用 id 模式。上一窗口未消费的旧 id 不能结束下一窗口的新事件。

点序列统计

mark(name, info?) 记录瞬时里程碑,不产生聚合桶:

ts
import { mark } from '@mpxjs/perf'

if (__mpx_perf_user__) {
  mark('goods:data-ready', {
    source: 'cache',
    itemCount: 20
  })
}

同名 mark 仍是多条独立事件。info 按引用保存,不复制、不校验、不序列化;建议只传小型、可序列化的诊断字段,不要传组件实例、完整 props、响应体或大数组。

数据结构

ts
interface AggrResult {
  count: number
  sum: number
  avg: number
  max: number
}

interface MarkEvent {
  name: string
  start: number
  timestamp: number
  info?: unknown
}

interface MarkTimeline {
  events: MarkEvent[]
  dropped: number
}

interface TraceEvent {
  name: string
  start: number
  timestamp: number
  duration: number
  info?: unknown
}

interface TraceTimeline {
  events: TraceEvent[]
  dropped: number
  incomplete: number
}

start 是相对当前录制窗口起点的毫秒偏移。timestampperformance.now()、Hermes nativePerformanceNow()Date.now() 返回的原始值,不保证是 Unix epoch;同一运行环境和窗口中的值可以直接比较。

mark 和 trace 使用独立容量。达到上限后只增加对应 dropped,不保存事件、时间、name 或 info。mark 始终为 end 边界预留最后一个位置。

Reporter

ts
type Reporter = (
  aggregates: Map<string, AggrResult>,
  marks?: MarkTimeline,
  traces?: TraceTimeline
) => void

通过正常 start/end 完成的窗口始终传入 marks 和 traces;后两个参数保持可选,兼容旧的一、二参数 Reporter 和外部手动调用。

ts
import { setReporter } from '@mpxjs/perf'
import type {
  AggrResult,
  MarkTimeline,
  TraceTimeline
} from '@mpxjs/perf'

if (__mpx_perf__) {
  setReporter((
    aggregates: Map<string, AggrResult>,
    marks?: MarkTimeline,
    traces?: TraceTimeline
  ) => {
    MyAPM.report('mpx_perf_aggregates', aggregates)
    if (marks) MyAPM.report('mpx_perf_marks', marks)
    if (traces) MyAPM.report('mpx_perf_traces', traces)
  })
}

全局 Reporter 和 end(localReporter) 传入的局部 Reporter 会依次收到同一份 Map、marks 和 traces 引用。不要直接修改;需要改写时自行复制。

注册时机

setReporter 必须直接放在 if (__mpx_perf__) 中,确保自定义函数和闭包也能在关闭态被 DCE。

Console Reporter

默认 consoleReporter 分别输出 aggregates、traces 和 marks。createConsoleReporter({ sortBy, filter, header }) 可定制:

  • sortBy 只影响 aggregates。
  • filter 同时作用于 aggregate、trace 和显式 mark,内建 start/end 不会隐藏。
  • trace 和 mark 保持原始顺序。
  • 仅有事件包含 info 时显示 info 列。
  • mark dropped、trace dropped 和 trace incomplete 分别提示。
  • info 的 JSON 格式化失败不会中断业务或其他统计输出。

Chrome Trace 对接

TraceEvent 可映射为 Chrome Trace / Perfetto Complete Event:

ts
const chromeEvents = traces.events.map(event => ({
  name: event.name,
  ph: 'X',
  ts: event.timestamp * 1000,
  dur: event.duration * 1000,
  args: event.info
}))

核心包不内置文件写入或格式转换器,业务 Reporter 可按需补充 pid / tid / cat 等字段。MarkEvent 可用同一 timestamp * 1000 规则映射为 Instant Event。

API 参考

API说明
aggrStart(name, useName?)默认返回数字 id;传 true 改用 name 配对。
aggrEnd(idOrName)完成区段并实时聚合;无效或重复目标安全 noop。
traceStart(name, useName?)默认返回数字 id;传 true 改用 name 配对。
traceEnd(idOrName, info?)完成 trace 并保存可选 info。
mark(name, info?)追加独立、有序的点事件。
start(options?)创建录制窗口和三类容器;重复 start 幂等。
end(reporter?)关闭窗口,回填 avg、压缩未完成 trace 并触发 Reporter。
setReporter(r) / clearReporter()替换或清空全局 Reporter。
createConsoleReporter(opts?)创建可配置的 Console Reporter。
consoleReporter默认 Reporter。

旧 API 兼容

旧 API内部映射兼容语义
scopeStart(name)aggrStart(name)返回 id;未录制返回 -1
scopeEnd(id)aggrEnd(id)负 id 和重复结束安全 noop。
measureStart(name)aggrStart(name, true)后一次同名 start 覆盖前一次。
measureEnd(name)aggrEnd(name)命中后消费起点。

四个旧函数继续导出且没有移除版本,但新代码优先使用 aggrStart/aggrEnd。聚合结果类型已由 AggResult 更名为 AggrResult,不保留旧类型别名;MarkEvent.at 已更名为 start,并新增 timestamp / info

内置框架探针事件 schema

现有框架探针继续通过兼容的 scopeStart/scopeEnd 采集同步 render 聚合耗时。

mpx-view

事件名覆盖代码段
view:render:total整个 forwardRef 回调
view:render:propssplitProps + 解构 + useHover
view:render:styleuseTransformStyle + splitStyle + 布局和动画 Hook
view:render:innerPropsuseInnerProps
view:render:createElementwrapWithChildren + createElement 收尾

mpx-simple-view

事件名覆盖代码段
simple-view:render:total整个函数
simple-view:render:stylesplitProps + splitStyle + 样式变换
simple-view:render:innerPropsuseInnerProps
simple-view:render:createElementwrapChildren + createElement 收尾

mpx-text

事件名覆盖代码段
text:render:total整个 forwardRef 回调
text:render:props文本上下文 + props 合并
text:render:style样式转换、继承与拆分
text:render:innerPropsuseInnerProps
text:render:createElementdecode + wrapChildren + createElement 收尾

mpx-simple-text

事件名覆盖代码段
simple-text:render:total整个函数
simple-text:render:style文本上下文、样式和 props 合并
simple-text:render:innerPropsuseInnerProps
simple-text:render:createElementwrapChildren + createElement 收尾

@mpxjs/core: __getStyle

事件名覆盖代码段
getStyle:total整个 __getStyle 函数
getStyle:classclass 解析与样式查找
getStyle:style静态/动态 style 解析与转换

性能影响评估

能力未录制录制中
aggr id 模式状态判断后返回 -1数组槽位 + free list;稳态零对象、零闭包分配
aggr name 模式保存 name 起点Map set/get/delete;只保留聚合桶
trace状态判断后 noop每个被接受区段一个事件对象和一条进行中映射,默认最多 1024 条
mark状态判断后 noop每个显式 mark 一个事件对象,默认最多 1022 条,加边界后最多 1024 条

聚合与事件序列的取舍

高频 render 和函数耗时使用 aggr;需要火焰图或逐区段瀑布图时使用 trace;里程碑使用 mark。trace/mark 不能替代渲染循环中的聚类统计。

与现有工具的关系

  • Hermes Profiler:提供 JS 函数级采样;trace 提供 Mpx 业务语义明确的区段序列,两者可使用相同 performance 时钟对照分析。
  • Perfetto / Chrome Trace:Perf 的 trace 是稳定数据源,业务 Reporter 负责转换和补充进程、线程、分类字段。
  • 业务 APM:Perf 不替代 APM,只提供聚合、区段和里程碑数据。

Terser / Babel 兼容性约束

  • 最终构建依赖 @mpxjs/perfdist/index.js 保留顶层三元、sideEffects: false 与使用方 Terser 完成 DCE。
  • 探针调用必须直接置于 if (__mpx_perf_framework__) / if (__mpx_perf_user__) 字面量条件内。
  • 接入方需保留默认 Terser 的 dead_code / conditionals 优化。
  • Babel 不应提前破坏 __mpx_perf__ ? impl.x : noop.x 顶层三元结构。