前端性能指标
前置知识:
从输入url到页面最终的呈现都发生了什么?需要进行现代框架与之前的区别。
window全局作用域下的一个API:Performance。
提出一个问题:
我们前端性能统计到底是以什么衡量?速度与内存。
得出统计性能的原理
所谓的性能统计,就是在合适的时机,打上合适的时间戳,或者暴露出事件。然后通过这些时间戳之间的差值,得出一个耗时时间。这个耗时时间就可以反映出我们页面的相关性能。那么我们如何知道知道这些时间点呢?这就需要我们去了解Performance
他在MDN是这样说的:
Performance接口可以获取到当前页面中与性能相关的信息。它是 High Resolution Time API 的一部分,同时也融合了 Performance Timeline API、Navigation Timing API、 User Timing API 和 Resource Timing API。
User Timing API:用户自己定义在代码中通过调用performance.mark(key)方法定义的时间点。
Navigation Timing API:资源请求的api
Resource Timing API:它里面包含的是我们从请求开始,到整个页面的完全显示的各个阶段的时间点,包含了以下:
| key值 | value值得解释 |
|---|---|
| navigationStart | 当前浏览器窗口的前一个网页关闭,发生unload事件时的时间戳。如果没有前一个网页,就等于fetchStart(也就是输入URL开始,第一步就是卸载上个页面) |
| redirectStart | 第一次重定向开始时的时间戳,如果没有重定向,或者上次重定向不是同源的。则为0 |
| redirectEnd | 最后一次重定向完成,也就是Http响应的最后一个字节返回时的时间戳。如果没有重定向,或者上次重定向不是同源的。则为0 |
| fetchStart | 浏览器准备通过HTTP请求去获取页面的时间戳。在检查应用缓存之前发生。 |
| domainLookupStart | 域名查询开始时的时间戳。如果使用持久连接,或者从本地缓存获取信息的,等同于fetchStart |
| domainLookupEnd | 域名查询结束时的时间戳。如果使用持久连接,或者从本地缓存获取信息的,等同于fetchStart |
| connectStart | HTTP请求开始向服务器发送时的时间戳,如果是持久连接,则等同于fetchStart。 |
| connectEnd | 浏览器与服务器之间的连接建立时的时间戳,连接建立指的是所有握手和认证过程全部结束 |
| requestStart | 浏览器向服务器发出HTTP请求时(或开始读取本地缓存时)的时间戳 |
| responseEnd | 浏览器从服务器收到(或从本地缓存读取)最后一个字节时(如果在此之前HTTP连接已经关闭,则返回关闭时)的时间戳 |
| responseStart | 浏览器从服务器收到(或从本地缓存读取)第一个字节时的时间戳。 |
| domLoading | 当前网页DOM结构开始解析时,也就是document.readyState属性变为“loading”、并且相应的readystatechange事件触发时的时间戳 |
| domInteractive | 当前网页DOM结构结束解析 |
| domContentLoadedEventStart | 当前网页DOMContentLoaded事件发生时,也就是DOM结构解析完毕、所有脚本开始运行时的时间戳 |
| domContentLoadedEventEnd | 当前网页DOMContentLoaded事件发生时,也就是DOM结构解析完毕、所有脚本运行完成时的时间戳 |
| domComplete | 当前网页DOM结构生成时,也就是Document.readyState属性变为“complete”, |
| loadEventStart | 当前网页load事件的回调函数开始时的时间戳。如果该事件还没有发生,返回0 |
| loadEventEnd | 当前网页load事件的回调函数结束时的时间戳。如果该事件还没有发生,返回0。 |
PerformanceObserver.observe():指定监测的 entry types 的集合。 当 performance entry 被记录并且是指定的 entryTypes 之一的时候,性能观察者对象的回调函数会被调用。
在有了上面这些属性之后,我们就可以实现一个简单的性能统计。
在现如今的前端开发中,我们着重关注以下几个性能,分别是:
白屏时间 FP
指的是当我输入URL开始,到页面开始有变化,只要有任意像素点变化,都算是白屏时间的完结。(根据图片显示)
function getFP() {
new PerformanceObserver((entryList, observer) => {
let entries = entryList.getEntries();
console.log(entries);
for (let i = 0; i < entries.length; i++) {
if (entries[i].name === 'first-paint') {
console.log('FP', entries[i].startTime);
}
}
const lastEntry = entries[entries.length - 1];
// observer.disconnect();
}).observe({entryTypes: ['paint']});
};
首次内容绘制FCP
-
:指的是页面上绘制了第一个元素。
FP与FCP的最大的区别就在于:FP 指的是绘制像素,比如说页面的背景色是灰色的,那么在显示灰色背景时就记录下了 FP 指标。但是此时 DOM 内容还没开始绘制,可能需要文件下载、解析等过程,只有当 DOM 内容发生变化才会触发,比如说渲染出了一段文字,此时就会记录下 FCP 指标。因此说我们可以把这两个指标认为是和白屏时间相关的指标,所以肯定是最快越好。
function getFP() { new PerformanceObserver((entryList, observer) => { let entries = entryList.getEntries(); console.log(entries); for (let i = 0; i < entries.length; i++) { if (entries[i].name === 'first-contentful-paint'){ console.log('FCP', entries[i].startTime); } } const lastEntry = entries[entries.length - 1]; // observer.disconnect(); }).observe({entryTypes: ['paint']}); };首次有效绘制FMP
这个东西具有一定的争议性,他指的是页面中有效内容绘制,对于每个网站对于有效内容的定义是不同的,因此一般不做研究。
首页时间
首页时间指的是,当onload事件触发的时候,也就是整个首页加载完成的时候。
function getFirstPage() { console.log('FIRSTPAGE', (performance.timing.loadEventEnd - performance.timing.fetchStart)); };最大内容绘制LCP
用于记录视窗内最大的元素绘制的时间,该时间会随着页面渲染变化而变化,因为页面中的最大元素在渲染过程中可能会发生改变,另外该指标会在用户第一次交互后停止记录。
function getLCP() { // 增加一个性能条目的观察者 new PerformanceObserver((entryList, observer) => { let entries = entryList.getEntries(); const lastEntry = entries[entries.length - 1]; // observer.disconnect(); console.log('LCP', lastEntry.renderTime || lastEntry.loadTime); }).observe({entryTypes: ['largest-contentful-paint']}); }首次可交互时间TTI
-
需要满足以下条件:
- 从 FCP 指标后开始计算
- 持续 5 秒内无长任务(执行时间超过 50 ms)且无两个以上正在进行中的 GET 请求
- 往前回溯至 5 秒前的最后一个长任务结束的时间
function getTTI() { let time = performance.timing.domInteractive - performance.timing.fetchStart; console.log('TTI', time); }; -
-
首次输入延迟(FID): 记录在 FCP 和 TTI 之间用户首次与页面交互时响应的延迟。
function getFID() { new PerformanceObserver((entryList, observer) => { let firstInput = entryList.getEntries()[0]; if (firstInput) { const FID = firstInput.processingStart - firstInput.startTime; console.log('FID', FID); } // observer.disconnect(); }).observe({type: 'first-input', buffered: true}); } -
累计位移偏移(CLS):大家想必遇到过这类情况:页面渲染过程中突然插入一张巨大的图片或者说点击了某个按钮突然动态插入了一块内容等等相当影响用户体验的网站。这个指标就是为这种情况而生的,计算方式为:位移影响的面积 * 位移距离。如下图:
0.25 * 0.75 = 0.1875。CLS 推荐值为低于 0.1
function getCLS() {
try {
let cumulativeLayoutShiftScore = 0;
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// Only count layout shifts without recent user input.
if (!entry.hadRecentInput) {
cumulativeLayoutShiftScore += entry.value;
}
}
});
observer.observe({type: 'layout-shift', buffered: true});
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
// Force any pending records to be dispatched.
observer.takeRecords();
observer.disconnect();
console.log('CLS:', cumulativeLayoutShiftScore);
}
});
} catch (e) {
// Do nothing if the browser doesn't support this API.
}
};
那么对于以上的性能有哪些是需要我们着重注意的呢?
现在前端框架中检测
web-vitals:是一个前端框架,他可以不需要你书写上面代码只需要轻松的引用即可。
缺点是:目前只能统计'CLS' | 'FCP' | 'FID' | 'LCP' | 'TTFB'。如果需要扩充的话,就可以使用上面的Performance进行更改。
在谷歌的标准中,一般我们只需要关心以上几个,分别是LCP, FID, CLS;
那么我们该如何去检测跟改善他们呢?
有哪些因素影响着这些性能
影响白屏的时间
首先我们会分为以下几个个对照组。
此次对比的方式是,采用谷歌network选项中的网络选项并勾选禁止缓存
html文档结构完全相同:网速快一点的 VS 网速慢一点的。.
html文档结构完全相同: JS异步加载 VS JS同步加载
网速相同:大型JS VS 小型 JS
有1张图片的 VS 有两张图片的 (首页)
文档结构相同,网速相同: 有缓存 VS 无缓存
由此可以推断出影响白屏,LCP,首页的主要原因有
- 网速问题
- JS包大小问题
- 是否启用了JS异步加载。
解决方案
- 提高带宽
- 需要使用webpack进行tree-shaking
- 使用路由懒加载,只有在使用的时候在进行路由加载
- 尽量使用CDN进行加速
- 建立缓存,提高下次加载速度。
- 开启gzip压缩。
影响用户可操作时间
这个我们与网速完全相同,并且body前不加入外部script:script标签内存在长任务 VS script标签内不存在长任务。操作步骤类似,最终经过试验,结论也是 存在长任务的时间要明显大于没有长任务的时间。
如何对于重点性能指标进行提升,都有哪些方法改进
对于白屏
我们在实验中已经分析过了,影响白屏的时间,就是网速,body前是否存在阻塞的script标签,以及是否存在长时间执行的任务。因此对症下药我们可以有一下解决方案:
- 部署CDN,缩短用户与节点之间的距离(网速)
- 提高带宽(网速)
- 不要在头部添加任何script标签等。
- 建立缓存,提高下次加载速度。
- 开启gzip压缩。
- 对于少量小图标(单个尽量不要超过10K的),我们可以使用url-loader打包。或者使用将图标转化为字体库,异步进行加载。
- 对于大图标的话,需要做到在展示的时候再去加载。也就是当图片出现到浏览器窗口的时候再去加载,而不是首屏的图片全部加载。
对于CLS
文档结构相同: 脱离文档流 VS 不脱离文档流
文档结构相同,不脱离文档流:使用transform: 不使用transform
文档结构相同,给图片指定宽高 VS 不给图片指定宽高
得出结论:
- 如果经常需要变动的元素,脱离文档流,或者是占据位置,只是隐藏。
- 对于位移等操作,使用动画代替。
- 在定义图片的时候,就应该给出具体的宽高。
对于用户可操作时间
对于用户可操作时间,影响一个是注册的事件是否可以被执行(说的通俗点就是JS脚本是否加载完毕),以及是否存在长任务。那么我们就可以有以下解决方案:
- 对文件进行懒加载,不要一次性把所有的JS加载出来。这就需要使用路由懒加载,在跳转到某个路由的时候,再去加载他的脚本资源。这样就可以保证JS加载速度的优化。
- 不要在响应事件里有过多的运算,导致卡顿。如果确有需要,应当开启webWorker,新起线程运算。
bigpipe
bigPipe是由facebook提出来的一种动态网页加载技术。它将网页分解成称为pagelets的小块,然后分块传输到浏览器端,进行渲染。它可以有效地提升首屏渲染时间。
可以看出,bigpipe的适用是服务端进行渲染,然后将一块一块的文件传递给前端。
那么为什么需要分块传输呢?
继续做对照试验: 分块传输 VS 不分块传输
可以明显的看出,在不进行分块传输的时候,需要经过漫长的等待,界面才能看到变化。ssr渲染的宗旨就是更快的渲染,然而在长任务时候,效果不理想。