1. 基本指标介绍
首先前端性能指标一般分为以下几种:
- 首屏绘制(First Paint,FP)
- 首屏内容绘制(First Contentful Paint,FCP)
- 可交互时间(Time to Interactive,TTI)
- 最大内容绘制(Largest Contentful Paint,LCP)
- 首次有效绘制(First Meaning Paint, FMP)
FP 是时间线上的第一个“时间点”,是指浏览器从响应用户输入网址地址,到浏览器开始显示内容的时间,简而言之就是浏览器第一次发生变化的时间。
FCP(全称“First Contentful Paint”,翻译为“首次内容绘制”),是指浏览器从响应用户输入网络地址,在页面首次绘制文本,图片(包括背景图)、非白色的 canvas 或者SVG 才算做 FCP,有些文章说 FCP 是首屏渲染事件,这其实是不对的。
TTI,翻译为“可交互时间”表示网页第一次完全达到可交互状态的时间点。可交互状态指的是页面上的 UI 组件是可以交互的(可以响应按钮的点击或在文本框输入文字等),不仅如此,此时主线程已经达到“流畅”的程度,主线程的任务均不超过50毫秒。在一般的管理系统中,TTI 是一个很重要的指标。
FMP(全称“First Meaningful Paint”,翻译为“首次有效绘制”表示页面的“主要内容”开始出现在屏幕上的时间点,它以前是我们测量用户加载体验的主要指标。本质上是通过一个算法来猜测某个时间点可能是 FMP,但是最好的情况也只有77%的准确率,在lighthouse6.0 的时候废弃掉了这个指标,取而代之的是 LCP 这个指标。
LCP(全称“Largest Contentful Paint”)表示可视区“内容”最大的可见元素开始出现在屏幕上的时间点。
2. performance介绍
performance 对象是专门用来用于性能监控的对象,内置了一些前端需要的性能参数。
2.1. performance.now()方法
performance.now()返回performance.navigationStart至当前的毫秒数。performance.navigationStart是下文将介绍到的可以说是浏览器访问最初的时间测量点。
2.2. performance.timing
2.3. performance.getEntries()方法
浏览器获取网页时,会对网页中每一个对象(脚本文件、样式表、图片文件等等)发出一个 HTTP 请求。performance.getEntries() 方法以数组形式,返回一个 PerformanceEntry 列表,这些请求的时间统计信息,有多少个请求,返回数组就会有多少个成员。
name :资源名称,是资源的绝对路径或调用mark方法自定义的名称 startTime :开始时间 duration :加载时间 entryType :资源类型,entryType 类型不同数组中的对象结构也不同!具体见下 initiatorType :谁发起的请求,具体见下:
值 |
描述 |
mark | 通过 mark() 方法添加到数组中的对象 |
paint | 通过 measure() 方法添加到数组中的对象 |
measure |
first-contentful-paint 首次内容绘制 |
resource | 所有资源加载时间,用处最多 |
3. 指标计算方法
3.1. 首屏和白屏
关于首屏和白屏的计算时间不同的说法比较多但大致相同,主要争论是关于首屏图片是否算首屏加载时间。
白屏:
白屏时间(First Paint):是指浏览器从响应用户输入网址地址,到浏览器开始显示内容的时间,一种比较简单的做法是在 body 标签之前获取当前时间 - performance.timing.navigationStart,或者直接获取 performance 中关于 paint 的两个数据,都可以直接作为白屏数据,这两个数据一般差别不大。
首次绘制 FP 包括了任何用户自定义的背景绘制,它是首先将像素绘制到屏幕的时刻。
首次内容绘制 FCP 是浏览器将第一个 DOM 渲染到屏幕的时间。该指标报告了浏览器首次呈现任何文本、图像、画布或者 SVG 的时间。
也可以使用其他的计算方法:白屏时间 = 页面开始展示的时间点 - 开始请求的时间点。
首屏:
首屏时间:是指浏览器从响应用户输入网络地址,到首屏内容渲染完成的时间,在需要展示的元素页面之前获取当前时间 - performance.timing.navigationStart。
首屏时间 = 首屏内容渲染结束时间点 - 开始请求的时间点 首屏计算的方法比较多,很多文章中使用的方法都不太一样 performance.timing.interactive - performance.timing.fetchStart 也有一些使用的是 performance.timing.loadEventEnd - performance.timing.navigationStart 不过时间差别应该不大,都是用从dom加载完毕减去请求开始或者刷新url的时间。
3.2. TTI
关于 TTI 可以首先了解下谷歌提出的性能模型 RAIL:
1.响应:输入延迟时间(从点按到绘制)小于 100 毫秒。用户点按按钮(例如打开导航)。
2.动画:每个帧的工作(从 JS 到绘制)完成时间小于 16 毫秒。用户滚动页面,拖动手指(例如,打开菜单)或看到动画。拖动时,应用的响应与手指位置有关(例如,拉动刷新、滑动轮播)。此指标仅适用于拖动的持续阶段,不适用于开始阶段。
3.空闲:主线程 JS 工作分成不大于 50 毫秒的块。用户没有与页面交互,但主线程应足够用于处理下一个用户输入。
4.加载:页面可以在 1000 毫秒内就绪。用户加载页面并看到关键路径内容。
我们可以通过domContentLoadedEventEnd来粗略的进行估算:
TTI:domContentLoadedEventEnd - navigationStart
谷歌实验室也提供了更加便捷准确的api包进行测算 tti-polyfil:
- import ttiPolyfill from './path/to/tti-polyfill.js';
- ttiPolyfill.getFirstConsistentlyInteractive(opts).then((tti) => {
- // Use `tti` value in some way.
- });
3.3. LCP
在过去,我们也有推荐的性能指标,如:FMP (First Meaningful Paint)和SI (Speed Index)可以帮我们捕获更多的首次渲染之后的加载性能,但这些过于复杂,而且很难解释,也经常出错,没办法确定主要内容什么时候加载完。
根据 W3C Web 性能工作组的讨论和 Google 的研究,发现度量页面主要内容的可见时间有一种更精准且简单的方法是查看 “绘制面积” 最大的元素何时开始渲染。
所谓绘制面积可以理解为每个元素在屏幕上的 “占地面积” ,如果元素延伸到屏幕外,或者元素被裁切了一部分,被裁切的部分不算入在内,只有真正显示在屏幕里的才算数。图片元素的面积计算方式稍微有点不同,因为可以通过 CSS 将图片扩大或缩小显示,也就是说,图片有两个面积:“渲染面积”与“真实面积”。在 LCP 的计算中,图片的绘制面积将获取较小的数值。例如:当“渲染面积”小于“真实面积”时,“绘制面积”为“渲染面积”,反之亦然。
页面在加载过程中,是线性的,元素是一个一个渲染到屏幕上的,而不是一瞬间全渲染到屏幕上,所以“渲染面积”最大的元素随时在发生变化。如果使用 PerformanceObserver 去捕获 LCP,会发现每当出现“渲染面积”更大的元素,就会捕获出一条新的性能条目。
如果元素被删除,LCP 算法将不再考虑该元素,如果被删除的元素刚好是 “绘制面积” 最大的元素,则使用新的 “绘制面积” 最大的元素创建一个新的性能条目。
该过程将持续到用户第一次滚动页面或第一次用户输入(鼠标点击,键盘按键等),也就是说,一旦用户与页面开始产生交互,则停止报告新的性能条目。
上面两张图都是在页面加载过程中,最大元素发生变化。第一张图,新的内容被加入到DOM,而且这个元素成为了最大元素。第二张图,布局发生了变化,之前在视窗中的元素被移出了视窗外。
可以直接使用 PerformanceObserver 来捕获 LCP:
- const observer = new PerformanceObserver((entryList) => {
- const entries = entryList.getEntries();
- const lastEntry = entries[entries.length - 1];
- const lcp = lastEntry.renderTime || lastEntry.loadTime;
- console.log('LCP:', lcp);
- });
- observer.observe({ entryTypes: ['largest-contentful-paint'] });
LCP也不是完美的,也很容易出错,它会在用户进行交互后就停止捕获,可能会获取到错误的结果,如果有占据页面很大的轮播图也会产生问题会不断的更新 LCP。
LCP也有现成的计算工具库 web-vitals:
- import {getLCP} from 'web-vitals';
- // Measure and log the current LCP value,
- // any time it's ready to be reported.
- getLCP(console.log);