小程序埋点与性能监控体系:从 wx.getPerformance 到线上告警
"线上有点卡"——这种反馈没法排查:哪个页面、哪个环节、什么机型、什么网络?没有监控体系的小程序,性能问题全靠猜。这篇搭一套完整的可观测方案:性能指标采集、业务埋点规范、异常捕获、上报通道和告警闭环。
一、监控什么:指标体系
1.1 性能指标(技术侧)
| 指标 | 采集方式 | 关注点 |
|---|---|---|
| 启动耗时 | wx.getPerformance | 下载/注入/首屏渲染分段 |
| 路由切换耗时 | 手动打点 | onShow 到可交互 |
| setData 耗时 | wx.getPerformance + 手动 | 单次通信量和频率 |
| 请求耗时 | 拦截器打点 | P95 接口慢查询 |
| 白屏率/错误率 | 异常捕获 | 稳定性红线 |
1.2 业务指标(产品侧)
漏斗模型:曝光 → 点击 → 提交 → 支付成功。技术指标告诉你"哪里慢",业务指标告诉你"哪里漏"。
二、性能指标采集
2.1 wx.getPerformance
// app.js onLaunch 里启动观察者
const perf = wx.getPerformance()
const observer = perf.createObserver((entryList) => {
entryList.getEntries().forEach(entry => {
switch (entry.name) {
case 'appLaunch': // 启动总耗时
report('perf_launch', {
duration: entry.duration,
path: entry.path,
scene: entry.scene // 进入场景值,很关键
})
break
case 'firstRender': // 首次渲染
report('perf_first_render', { duration: entry.duration })
break
case 'evaluateScript': // 逻辑层注入
report('perf_evaluate', { duration: entry.duration })
break
}
})
})
observer.observe({ entryTypes: ['navigation', 'render', 'script'] })启动耗时的正确拆解:appLaunch = 代码包下载 + 代码注入 + 首次渲染 + 首屏接口。分别打点才能定位瓶颈:下载慢是包体积问题,注入慢是主包代码量问题,渲染慢是首屏 setData 数据量问题,接口慢是后端问题——处方不同,诊断必须分开。
2.2 页面级打点
// utils/perf-track.js
function trackPageShow(page) {
page._showAt = Date.now()
}
function trackPageReady(page) {
const cost = Date.now() - page._showAt
report(`perf_page_${page.route}`, { cost })
}
// 劫持 Page 构造器统一埋点(一次接入全局生效)
const originPage = Page
Page = function (config) {
const { onShow, onReady } = config
config.onShow = function () {
trackPageShow(this)
onShow && onShow.call(this)
}
config.onReady = function () {
trackPageReady(this)
onReady && onReady.call(this)
}
originPage(config)
}2.3 setData 监控
高频 setData 是性能杀手,全局埋点监控:
// 劫持 setData,记录每次调用数据量
function wrapSetData(page) {
const origin = page.setData.bind(page)
page.setData = function (data, callback) {
const size = JSON.stringify(data).length
if (size > 100 * 1024) { // 单次超 100KB 告警
report('perf_setdata_oversize', {
page: page.route, size,
keys: Object.keys(data).slice(0, 10)
})
}
const start = Date.now()
origin(data, () => {
callback && callback()
report('perf_setdata', { page: page.route, size, cost: Date.now() - start })
})
}
}三、业务埋点规范
3.1 事件模型
统一 schema,别每个页面自由发挥:
// 埋点 = 谁 + 在哪 + 做了什么 + 结果如何
track('order_submit', {
page: 'pages/order/confirm', // 页面路径
biz_params: { // 业务参数
goodsCount: 3,
amount: 99.0,
couponUsed: true
},
result: 'success', // success / fail
failReason: '' // 失败原因
})3.2 曝光埋点的坑
列表项曝光不能用 onShow(整页级别太粗),用 IntersectionObserver:
// 组件 attach 时注册观察器,进入视口才算曝光
Component({
lifetimes: {
attached() {
this._observer = this.createIntersectionObserver()
this._observer.relativeToViewport({ bottom: 0 }).observe('.card', (res) => {
if (res.intersectionRatio > 0.5 && !this._exposed) {
this._exposed = true // 只曝光一次
track('goods_expose', { id: this.data.item.id })
this._observer.disconnect() // 观察完就断开,省性能
}
})
},
detached() { this._observer && this._observer.disconnect() }
}
})3.3 漏斗分析
把关键路径事件串起来,服务端按 session_id 聚合:
session: abc123
10:00:01 home_banner_expose
10:00:05 home_banner_click
10:00:12 goods_detail_view
10:00:40 cart_add
10:01:15 order_submit (success)
10:01:30 pay_success转化率 = 后一环节人数 / 前一环节人数。掉率异常的环节,就是产品和技术共同盯的靶子。
四、异常捕获
4.1 JS 错误
// app.js
App({
onError(err) {
report('error_js', {
message: err.message,
stack: err.stack.slice(0, 2000), // 截断,别把上报接口自己打爆
route: getCurrentPageRoute(),
version: APP_VERSION // 版本号必须带,定位灰度问题
})
}
})4.2 Promise 拒绝
// onError 捕获不到未处理的 Promise rejection
App({
onUnhandledRejection(res) {
report('error_promise', {
reason: String(res.reason).slice(0, 1000)
})
}
})4.3 请求失败
请求封装层统一捕获(业务错误码 + 网络错误分类上报):
function request(options) {
return new Promise((resolve, reject) => {
wx.request({
...options,
success: (res) => {
if (res.statusCode >= 500) {
report('error_http_5xx', { url: options.url, code: res.statusCode })
}
resolve(res)
},
fail: (err) => {
report('error_network', { url: options.url, msg: err.errMsg })
reject(err)
}
})
})
}JSError 聚合:上报时对 stack 做 fingerprint(取函数名+文件名哈希),服务端按 fingerprint 聚合数量,否则同一个错误刷出几万条没法看。
五、上报通道设计
5.1 批量 + 采样
// utils/report.js
const queue = []
let timer = null
function report(event, data) {
// 采样:非错误类事件 30% 采样率足够
if (!event.startsWith('error') && Math.random() > 0.3) return
queue.push({
event, data,
ts: Date.now(),
sessionId: getApp().globalData.sessionId,
openid: getApp().globalData.openid || '',
systemInfo: getApp().globalData.systemInfo, // 机型/系统/基础库,启动时采集一次缓存
version: APP_VERSION
})
// 攒 10 条或 5 秒批量发
if (queue.length >= 10 || timer) return
timer = setTimeout(flush, 5000)
}
function flush() {
timer = null
if (!queue.length) return
const batch = queue.splice(0, 20)
wx.request({
url: 'https://track.example.com/batch',
method: 'POST',
data: { events: batch },
fail: () => {
// 失败回队,最多重试一次,别无限堆积
if (queue.length < 200) queue.unshift(...batch)
}
})
}设计要点:
- 批量上报:逐条上报浪费请求数,也容易被限流
- 错误类全量、行为类采样:监控成本和有效性平衡
- 体积上限:队列封顶,极端情况下丢弃最旧数据,防止内存堆积
- onHide 时强制 flush:切后台立刻发送,别等定时器
5.2 上报域名要求
域名必须在微信后台配置 request 合法域名,且用独立子域名(track.xxx.com)——与业务接口隔离,统计流量波动不影响业务,业务接口故障也不影响监控数据回收。
六、告警闭环
采集只是开始,告警才产生价值:
| 规则 | 阈值 | 动作 |
|---|---|---|
| JSError 突增 | 5 分钟内同 fingerprint > 100 | 立即电话/短信 |
| 接口 5xx 率 | > 1% 持续 5 分钟 | 群告警 |
| 启动 P90 | > 5s 持续 30 分钟 | 群告警 |
| 支付成功率 | 环比跌 10% | 立即电话 |
告警分级是关键:全是紧急告警等于没有告警。资损类电话叫醒,体验类群消息,趋势类日报周报。
七、避坑清单
| 坑 | 现象 | 解法 |
|---|---|---|
| 埋点代码入侵业务 | 改个埋点改十处 | 劫持 Page/请求层统一埋点 |
| 上报打爆自己 | 错误风暴连环上报 | fingerprint 聚合 + 队列上限 |
| 没带版本号 | 无法定位是哪个版本的问题 | 全事件带 APP_VERSION |
| 曝光重复统计 | 滚动来回算多次 | _exposed 标记一次性 |
| 白名单域名忘了配 | 上报静默失败 | 上报 fail 打本地日志 |
| 只看均值 | 长尾用户被平均 | 看分位数 P90/P95 |
写在最后
监控体系的建设顺序建议:先异常(保命)→ 再性能(体检)→ 最后业务(增长)。异常监控上线第一天就能发现存量 bug;性能基线建立后,每次发版有数据回归;业务漏斗则是产品迭代的弹药库。三步都不复杂,难的是坚持让"数据说话"成为团队习惯。
评论 (0)