性能采集相关指标
- 首次绘制 (FP)
- 首次内容绘制 (FCP)
- 最大内容绘制 (LCP)
- 首次输入延迟 (FID)
- 累计布局偏移 (CLS)
- 总阻塞时间 (TBT)
首次绘制 (FP)
代表浏览器第一次向屏幕传输像素的时间,也就是页面在屏幕上首次发生视觉变化的时间。
首次内容绘制 (FCP)
FCP代表浏览器第一次向屏幕绘制来自DOM的“内容”(只有首次绘制文本、图片(包含背景图)、非白色的canvas或者SVG时才被算作FCP)
最大内容绘制 (LCP)
https://w3c.github.io/largest-contentful-paint/
LCP在用户滚动或单击按钮之前最大元素(通常是图像)出现在可视区域中所花费的时间。
First Paint 和 First Contentful Paint 等现有指标侧重于初始呈现,但没有考虑绘制内容的重要性,因此可能表明用户仍然认为页面没有用的时间。
- 计算范围
- image、video
- 带有background-image 的元素
- 文本节点组
上报时机
渲染每一帧做检测,绘制完第一帧立即上报一次,此后如果有新的元素比他大就再次上报,直到用户与页面进行交互为止
- 文本元素没渲染完不算(比如字体文件加载比较慢)
- 删除的内容是最大的,则只有在添加更大的内容时才会继续触发
首次输入延迟 (FID)
FID指的是用户首次与页面进行交互时,需要多长时间才会给出反馈。
计算FID比较简单,我们只需要在网页的head标签里注册一个事件(click、mousedown、keydown、touchstart、pointerdown),然后在事件响应函数中使用当前时间减去事件对象被创建的时间即可。performance.now() - event.timeStamp
累积布局偏移(CLS)
CLS 测量元素如何移动或页面布局的稳定性。 它考虑了内容的大小和移动的距离。 每当一个可见元素的位置从一个已渲染帧变更到下一个已渲染帧时,就发生了布局偏移 。
布局偏移分数
浏览器在计算布局偏移分数时,会查看可视区域大小和两个已渲染帧之间的可视区域中不稳定元素的位移。 小于500ms内发生的布局偏移会打上hadRecentInput标签,以便排除计算。
布局偏移分数的计算方式 布局偏移分数 = 影响分数 * 距离分数
假设一个元素占据了整个页面高度的 60%,然后移动了自身的一半:
视口的影响区域为 90% (60% + 30%),影响分数为 0.9(90%vh),距离分数为 0.3(30%vh)。
最终的计算结果是 0.9 x 0.3 = 0.27
一般认为小于0.1是比较好的分数,优化方式通常可以对图片和动态加载的元素提前预留空间,对于自定义字体可以采用
<link rel=”preload”>
和 font-display: optional
。
总阻塞时间 (TBT)
阻塞总时间,记录在 FCP 到 TTI 之间所有⻓任务的阻塞时间总和
每当出现长任务(在主线程上运行超过 50 毫秒的任务)时,主线程都被视作"阻塞状态"。
- 长任务
- 事件循环任务加上紧随其后的执行微任务检查点。这会捕获事件循环任务的持续时间,包括其关联的微任务。
- 更新事件循环处理模型中的渲染步骤。
- 事件循环处理模型的最后一步和下一个第一步之间的停顿。这会捕获用户代理在事件循环之外的 UI 线程中执行的任何工作。
上述四个性能指标都需要通过 PerformanceObserver 来获取
// 使用 PerformanceObserver 监听 fcp
if (!!PerformanceObserver){
try {
const type = 'paint';
if ((PerformanceObserver.supportedEntryTypes || []).includes(type)) {
observer = new PerformanceObserver((entryList)=>{
for(const entry of entryList.getEntriesByName('first-contentful-paint')){
const { startTime,duration } = entry;
console.log('[assets-load-monitor] PerformanceObserver fcp:', startTime+duration);
// 上报startTime操作
}
});
observer.observe({
entryTypes: [type],
});
return;
}
} catch (e) {
}
}
生产环境计算,可以采用 web-vitals 或者 Perfume.js
首次绘制有意义内容时间(FMP)
当整体页面的布局和文字内容全部渲染完成后,即可认为是完成了首次有意义内容的绘制,也就是所说的首屏时间。
单页应用浏览器需要先加载 JS , 然后再通过 JS 来渲染页面内容,这个时候首屏才算渲染完成。所以 Performance 已无法准确的监控到页面的首屏时间。
我们认为 DOM 结构变化的时间点与之对应渲染的时间点近似相同,也就是 FMP 的时间点为 DOM 结构变化最剧烈的时间点。DOM 结构变化的时间点可以利用 MutationObserver 来获取, 然后计算当前时刻 Dom 的分数。
计算分数主要流程:
- 从 body 元素开始递归计算。
- 排查无用的元素标签。
- 如果元素超出屏幕就认为是 0 分。
- 第一层的元素是 1 分,第二次的元素是 1 + (层数 * 0.5),也就是 1.5 分,依次类推,最终得打整个 Dom 数的总体分数。
function calculateScore(el, tiers, parentScore) {
try {
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;
} catch (error) {
}
}
通过 MutationObserver 得到了一个数组,数组的每一项就是每次 Dom 变化的时间和分数。
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
});
}
}
计算FMP会有一定性能消耗,且因为一些细小的变化导致数值产生波动,所以首屏也可以同时参考LCP。
传统 Performance.timing 耗时计算
- DNS 查询耗时:domainLookupEnd - domainLookupStart
- TCP 连接耗时:connectEnd - connectStart
- 内容加载耗时:responseEnd - requestStart
- firstbyte(首包时间):responseStart – domainLookupStart
- fpt(First Paint Time 首次渲染时间 / 白屏时间):responseEnd – fetchStart
- tti(Time to Interact 首次可交互时间):domInteractive – fetchStart
- ready(HTML 加载完成时间):domContentLoaded – fetchStart
- load(页面完全加载时间):loadEventStart – fetchStart
性能数据上报
数据上报可以使用以下几种方式:
- sendBeacon
- XMLHttpRequest
- image