# 一、性能优化方法论

首屏时间可以拆分为白屏时间、数据接口响应时间、图片加载资源等


# 二、指标采集:首屏时间指标采集具体办法
# 手动采集办法及优缺点:
- 所谓手动采集,一般是通过埋点的方式进行, 比如在页面开始位置打上
FMP.Start(),在首屏结束位置打上FMP.End(),利用FMP.End()-FMP.Start()获取到首屏时间。 - 手动采集的统计结果并不精确,因为依赖于人,每个人对首屏的理解有偏差,经常打错或者忘记打点。
# 自动化采集优势及办法
所谓自动化采集,即引入一段通用的代码来做首屏时间自动化采集,引入过程中,除了必要的配置不需要做其他事情。
自动化采集的好处是独立性更强,接入过程更自动化。具体的自动化采集代码,可以由一个公共团队来开发,试点后,推广到各个业务团队。而且统计结果更标准化,同一段统计代码,标准更统一,业务侧同学也更认可这个统计结果。
当然,它也有缺点,最明显的是,有些个性化需求无法满足,毕竟在工作中,总会有一些特殊业务场景。所以,采用自动化采集方案必须做一些取舍。
# 单页面(SPA)应用业务下的采集办法
SPA 页面因为无法基于 DOMContentLoaded 做首屏指标采集,可以使用 MutationObserver 采集首屏时间。
MutationObserver接口提供了监视对 DOM 树所做更改的能力。它被设计为旧的Mutation Events功能的替代品,该功能是 DOM3 Events 规范的一部分。
简单来说, 使用 MutationObserver 能监控页面信息的变化,当页面 body 变化最剧烈的时候,我们拿到的时间数据,就是首屏时间。
首先,在用户进入页面时,我们可以使用 MutationObserver 监控 DOM 元素 (Document Object Model,文档对象模型)。当 DOM 元素发生变化时,程序会标记变化的元素,记录时间点和分数,存储到数组中。数据的格式类似于 [200ms,18.5]。
为了提升计算的效率,我们认为首屏指标采集到某些条件时,首屏渲染已经结束,我们需要考虑首屏采集终止的条件,即计算时间超过 30 秒还没有结束;计算了 4 轮且 1 秒内分数不再变化;计算了 9 次且分数不再变化。
接下来,设定元素权重计算分数。
递归遍历 DOM 元素及其子元素,根据子元素所在层数设定元素权重,比如第一层元素权重是 1,当它被渲染时得 1 分,每增加一层权重增加 0.5,比如第五层元素权重是 3.5,渲染时给出对应分数。
为什么需要权重呢?
因为页面中每个 DOM 元素对于首屏的意义是不同的,越往内层越接近真实的首屏内容,如图片和文字,越往外层越接近 body 等框架层。
最后,根据前面的得分,计算元素的分数变化率,获取变化率最大点对应的分数。然后找到该分数对应的时间,即为首屏时间。
分数部分核心计算逻辑是递归遍历元素,将一些无用的标签排除,如果元素超过可视范围返回 0 分,每一层增加 0.5 的权重,具体请看下面代码示例。
function CScor(el, tiers, parentScore) {
let score = 0;
const tagName = el.tagName;
if ("SCRIPT" !== tagName && "STYLE" !== tagName && "META" !== tagName && "HEAD" !== tagName) {
const childrenLen = el.children ? el.children.length : 0;
if (childrenLen > 0) for (let childs = el.children, len = childrenLen - 1; len >= 0; len--) {
score += calculateScore(childs[len], tiers + 1, score > 0);
}
if (score <= 0 && !parentScore) {
if (!(el.getBoundingClientRect && el.getBoundingClientRect().top < WH)) return 0;
}
score += 1 + .5 * tiers;
}
return score;
}
变化率部分核心计算逻辑是获取 DOM 变化最大时对应的时间,代码如下所示
calFinallScore() {
try {
if (this.sendMark) return;
const time = Date.now() - performance.timing.fetchStart;
var isCheckFmp = time > 30000 || SCORE_ITEMS && SCORE_ITEMS.length > 4 && time - (SCORE_ITEMS && SCORE_ITEMS.length && SCORE_ITEMS[SCORE_ITEMS.length - 1].t || 0) > 2 * CHECK_INTERVAL || (SCORE_ITEMS.length > 10 && window.performance.timing.loadEventEnd !== 0 && SCORE_ITEMS[SCORE_ITEMS.length - 1].score === SCORE_ITEMS[SCORE_ITEMS.length - 9].score);
if (this.observer && isCheckFmp) {
this.observer.disconnect();
window.SCORE_ITEMS_CHART = JSON.parse(JSON.stringify(SCORE_ITEMS));
let fmps = getFmp(SCORE_ITEMS);
let record = null
for (let o = 1; o < fmps.length; o++) {
if (fmps[o].t >= fmps[o - 1].t) {
let l = fmps[o].score - fmps[o - 1].score;
(!record || record.rate <= l) && (record = {
t: fmps[o].t,
rate: l
});
}
}
//
this.fmp = record && record.t || 30001;
try {
this.checkImgs(document.body)
let max = Math.max(...this.imgs.map(element => {
if(/^(\/\/)/.test(element)) element = 'https:' + element;
try {
return performance.getEntriesByName(element)[0].responseEnd || 0
} catch (error) {
return 0
}
}))
record && record.t > 0 && record.t < 36e5 ? this.setPerformance({
fmpImg: parseInt(Math.max(record.t , max))
}) : this.setPerformance({});
} catch (error) {
this.setPerformance({});
// console.error(error)
}
} else {
setTimeout(() => {
this.calFinallScore();
}, CHECK_INTERVAL);
}
} catch (error) {
}
}
这个就是首屏计算的部分流程。
看完前面的流程,不知道你有没有这样的疑问:如果页面里包含图片,使用上面的首屏指标采集方案,结果准确吗?
结论是:不准确。上述计算逻辑主要是针对 DOM元素来做的,图片加载过程是异步,图片容器(图片的 DOM 元素)和内容的加载是分开的,当容器加载出来时,内容还没出来,一定要确保内容加载出来,才算首屏。
这就需要增加一些策略了,以下是包含图片页面的首屏计算 demo。
它的计算逻辑是这样的。
首先,获取页面所有的图片路径。在这里,图片类型分两种,一种是带 IMG 标签的,一种是带 DIV 标签的。前者可以直接通过 src 值得到图片路径,后者可以使用 window.getComputedStyle(dom) 方式获取它的样式集合。
接下来,通过正则获取图片的路径即可。
然后通过 performance.getEntriesByName(element)[0].responseEnd 的方式获取到对应图片路径的下载时间,最后与使用 MutationObserver 获得的 DOM 首屏时间相比较,哪个更长,哪个就是最终的首屏时间。
以上就是首屏采集的完整流程
注意:计算首屏时间用到的
getBoundingClientRect和getComputedStyle会引起强制回流

# 三、指标采集:白屏、卡顿、网络环境指标采集方法
# 白屏指标采集
白屏时间是指从输入内容回车(包括刷新、跳转等方式)后,到页面开始出现第一个字符的时间。白屏时间的长短会影响用户对 App 或站点的第一印象
白屏指标怎么采集呢?我们先来回顾一下浏览器的页面加载过程:
客户端发起请求 -> 下载 HTML 及 JS/CSS 资源 -> 解析 JS 执行 -> JS 请求数据 -> 客户端解析 DOM 并渲染 -> 下载渲染图片-> 完成渲整体染
在这个过程中,客户端解析 DOM 并渲染之前的时间,都算白屏时间。所以,白屏时间的采集思路如下:白屏时间 = 页面开始展示时间点 - 开始请求时间点。如果你是借助浏览器的 Performance API 工具来采集,那么可以使用公式:白屏时间 FP = domLoading - navigationStart。
这是浏览器页面加载过程,如果放在 App场景下,就不太一样了,App下的页面加载过程:
初始化 WebView -> 客户端发起请求 -> 下载 HTML 及 JS/CSS 资源 -> 解析 JS 执行 -> JS 请求数据 -> 服务端处理并返回数据 -> 客户端解析 DOM 并渲染 -> 下载渲染图片 -> 完成整体渲染。
App下的白屏时间,多了启动浏览器内核,也就是 Webview 初始化的时间。这个时间必须通过手动采集的方式来获得,而且因为线上线下时间差别不大,线下采集即可。具体来说,在 App 测试版本中,程序在 App 创建 WebView 时打一个点,然后在开始建立网络连接打一个点,这两个点的时间差就是 Webview 初始化的时间。
# 卡顿指标采集
所谓卡顿,简单来说就是页面出现卡住了的不流畅的情况。 提到它的指标,你是不是会一下就想到 FPS(Frames Per Second,每秒显示帧数)?FPS 多少算卡顿?网上有很多资料,大多提到 FPS 在 60 以上,页面流畅,不卡顿。但事实上并非如此,比如我们看电影或者动画时,素虽然 FPS 是 30 (低于60),但我们觉得很流畅,并不卡顿。
FPS 低于 60 并不意味着卡顿,那 FPS 高于 60 是否意味着一定不卡顿呢?比如前 60 帧渲染很快(10ms 渲染 1 帧),后面的 3 帧渲染很慢( 20ms 渲染 1 帧),这样平均起来 FPS 为95,高于 60 的标准。这种情况会不会卡顿呢?实际效果是卡顿的。因为卡顿与否的关键点在于单帧渲染耗时是否过长。
以 H5 为例,H5 场景下获取 FPS 方案如下:
如果连续判断 3次 FPS 都小于20,就认为是卡顿。

# 四、工具实践:性能 SDK 及上报策略设计
