页面帧(Performance -> FPS,Rendering -> FPS meter)、事件(Performance -> Event Log)和实际内存(Memory,Performance Monitor -> JS heap size)使用三个方面发现程序的问题。
如果一个页面比较卡,可能是由以下几方面造成的:
- 内存泄露。使用 Memory 的3次快照查看总内存的增加情况
- JS脚本的执行时间过长。使用 Performance 查看函数的执行时长,定位出具体函数
一、性能监控
1. 最简单的性能监控:计算首屏加载时间
<!DOCTYPE html>
<html>
<head>
<title></title>
<script type="text/javascript">
// 记录页面加载开始时间
var timerStart = Date.now();
</script>
<!-- 加载静态资源,如样式资源 -->
</head>
<body>
<!-- 加载静态JS资源 -->
<script type="text/javascript">
document.addEventListener('DOMContentLoaded', function() {
console.log("DOM 挂载时间: ", Date.now() - timerStart);
// 性能日志上报
});
window.addEventListener('load', function() {
console.log("所有资源加载完成时间: ", Date.now()-timerStart);
// 性能日志上报
});
</script>
</body>
</html>
2. 用 window.performance 进行性能监控
可以从 Performance 对象去获取性能指标数据。
建议在 window.onload 事件中读取各种数据,因为很多值必须在页面完全加载之后才能得到。
常用属性
timeOrigin
返回性能测量开始时的时间戳。
timing(PerformanceTiming)对象
单位毫秒
1. navigationStart
表示从同一个浏览器上下文的上一个网页卸载(unload)结束时的时间戳。如果没有上一个网页,这个值会和 fetchStart 相同。
2. unloadEventStart
表示前一个网页(与当前页面同域)unload 事件处理开始时的时间戳,如果无前一个网页或者前一个网页与当前页面不同域,则值为 0。
3. unloadEventEnd
和 unloadEventStart 相对应,表示unload
事件处理完成时的时间戳。如果没有前一个网页,或者前一个网页与当前页面不同域,这个值会返回0。
4. fetchStart
表示浏览器准备好使用HTTP请求来获取(fetch)文档的时间戳。这个时间点会在检查任何应用缓存之前。
5. requestStart
返回浏览器向服务器发出HTTP请求时(或开始读取本地缓存时)的时间戳。
6. responseStart
返回浏览器从服务器收到(或从本地缓存读取)第一个字节时的时间戳。
7. responseEnd
返回浏览器从服务器收到(或从本地缓存读取,或从本地资源读取)最后一个字节时(如果在此之前HTTP连接已经关闭,则返回关闭时)的时间戳。
8. domLoading
返回当前网页DOM结构开始解析时(即 document.readyState 属性变为“loading”、相应的 readystatechange
事件触发时)的时间戳。
9. domInteractive
返回当前网页DOM树结束解析、开始加载内嵌资源时(即Document.readyState属性变为“interactive”、相应的readystatechange
事件触发时)的时间戳。
注意: 只是 DOM 树解析完成,这时候并没有开始加载网页内的资源。
10. domContentLoadedEventStart
返回 DOM 解析完成后,网页内资源加载开始的时间,文档发生 DOMContentLoaded 事件的时间。
11. domContentLoadedEventEnd
返回 DOM 解析完成后,网页内资源加载完成的时间(如 JS 脚本加载执行完毕),文档的DOMContentLoaded 事件的结束时间。
12. domComplete
返回当前文档解析完成,即Document.readyState 变为 'complete'且相对应的
readystatechange
被触发时的时间戳。
13. loadEventStart
返回该文档下,load
事件被发送时的时间戳。如果这个事件还未被发送,它的值将会是0。
14. loadEventEnd
返回当load
事件结束,即加载事件完成时的时间戳。如果这个事件还未被发送,或者尚未完成,它的值将会是0。
// 计算加载时间
function getPerformanceTiming() {
var performance = window.performance;
if (!performance) {
// 当前浏览器不支持
console.log('你的浏览器不支持 performance 接口');
return;
}
var t = performance.timing;
var times = {};
//【重要】DNS 解析
//【原因】DNS 预加载做了么?页面内是不是使用了太多不同的域名导致域名查询的时间太长?
// 可使用 HTML5 Prefetch 预查询 DNS
times.lookupDomain = t.domainLookupEnd - t.domainLookupStart;
// 总体网络交互耗时:
times.network = t.responseEnd - t.navigationStart
// 渲染处理:
times.processing = (t.domComplete || t.domLoading) - t.domLoading
// 可交互:
times.active = t.domInteractive - t.navigationStart
//【重要】页面加载完成的时间
times.loadPage = t.loadEventEnd - t.navigationStart;
//【重要】解析 DOM 树结构的时间
//【原因】反省下你的 DOM 树嵌套是不是太多了!
times.domReady = t.domComplete - t.domInteractive;
//【重要】读取页面第一个字节的时间
//【原因】这可以理解为用户拿到你的资源占用的时间,加异地机房了么,加CDN 处理了么?加带宽了么?加 CPU 运算速度了么?
// TTFB 即 Time To First Byte 的意思
// 维基百科:https://en.wikipedia.org/wiki/Time_To_First_Byte
times.ttfb = t.responseStart - t.navigationStart;
//【重要】响应耗时
//【原因】页面内容经过 gzip 压缩了么,静态资源 css/js 等压缩了么?
times.request = t.responseEnd - t.responseStart;
//【重要】执行 onload 回调函数的时间
//【原因】是否太多不必要的操作都放到 onload 回调函数里执行了,考虑过延迟加载、按需加载的策略么?
times.loadEvent = t.loadEventEnd - t.loadEventStart;
// TCP 建立连接完成握手的时间
times.connect = t.connectEnd - t.connectStart;
// 白屏时间
times.domContentLoaded = t.domContentLoadedEventEnd - t.navigationStart;
return times;
}
navigation (PerformanceNavigation
)对象
呈现了如何导航到当前文档的信息。
1. type
表示是如何导航到这个页面的。
type 值 | 描述 |
---|---|
0 | 普通进入,包括:点击链接、书签、在地址栏中输入 URL、表单提交、或者通过除下表中 1 和 2 的方式初始化脚本。 |
1 | 通过刷新进入,包括:浏览器的刷新按钮、快捷键刷新、location.reload()等方法。 |
2 | 通过操作历史记录进入,包括:浏览器的前进后退按钮、快捷键操作、history.forward()、history.back()、history.go(num)。 |
255 | 其他非以上类型的方式进入 |
2. redirectCount
表示在到达这个页面之前重定向了多少次(这个接口有同源策略限制,即仅能检测同源的重定向)。
memory 对象
memory 是非标准属性,只在 Chrome 有,这个属性提供了一个可以获取到基本内存使用情况的对象。
jsHeapSizeLimit
: 内存大小限制totalJSHeapSize
: 可使用的内存usedJSHeapSize
: JS对象(包括V8引擎内部对象)占用的内存,不能大于totalJSHeapSize,如果大于,有可能出现了内存泄漏
onresourcetimingbufferfull 方法
它是一个在resourcetimingbufferfull
事件触发时会被调用的event handler
。这个事件当浏览器的资源时间性能缓冲区已满时会触发。可以通过监听这一事件触发来预估页面crash
,统计页面crash
概率,以便后期的性能优化,如下示例所示:
function buffer_full(event) {
console.log("WARNING: Resource Timing Buffer is FULL!");
performance.setResourceTimingBufferSize(200);
}
function init() {
// Set a callback if the resource buffer becomes filled
performance.onresourcetimingbufferfull = buffer_full;
}
<body onload="init()">
二、使用 Lighthouse 调试性能并优化
Lighthouse 是 Chrome 浏览器 devtools 里的其中一项功能。
Lighthouse 会衡量以下性能指标项:
- 首次内容绘制(First Contentful Paint)。即浏览器首次将任意内容(如文字、图像、canvas 等)绘制到屏幕上的时间点。
- 可交互时间(Time to Interactive)。指的是所有的页面内容都已经成功加载,且能够快速地对用户的操作做出反应的时间点。
- 速度指标(Speed Index)。衡量了首屏可见内容绘制在屏幕上的速度。在首次加载页面的过程中尽量展现更多的内容,往往能给用户带来更好的体验,所以速度指标的值约小越好。
- 总阻塞时间(Total Blocking Time)。指First Contentful Paint 首次内容绘制 (FCP)与Time to Interactive 可交互时间 (TTI)之间的总时间
- 最大内容绘制(Largest Contentful Paint)。度量标准报告视口内可见的最大图像或文本块的呈现时间
- 累积布局偏移(# Cumulative Layout Shift)。衡量的是页面整个生命周期中每次元素发生的非预期布局偏移得分的总和。每次可视元素在两次渲染帧中的起始位置不同时,就说是发生了LS(Layout Shift)。
在一般情况下,由于性能监控平台的和本地平台的差异,本地可能要达到70分,线上才有可能达到及格的状态,如果有性能优化的需求时,大家酌情处理即可(不过本人觉得,及格即可, 毕竟大学考试有曰:60分万岁,61分浪费)。
通用性能优化分析
我们知道 lighthouse 中有六个性能指标,而在这六个指标中,LCP、 FCP、speed index
这三个指数尤为重要,因为在一般情况下 这个三个指标会影响 TTI、TBT、CLS
的分数。
所以在我们在优化时, 需要提高LCP、 FCP和speedIndex 的分数,经过测试, 即使是空页面也会有时间上的损耗, 初始分数基本都是0.8
秒。
接下来我们就从LCP、 FCP和speedIndex 这三个指标入手
FCP(First Contentful Paint)
顾名思义就是首次内容绘制
,也就是页面最开始绘制内容的时间,由于我们现在开发的页面都是spa应用,所以,框架层面的初始化是一定会有一定的性能损耗
的,以vue-cli 搭建的脚手架为例,当我初始化空的脚手架,打包后上传cdn部署,FCP 就会从0.8s提上到1.5秒,由此可见vue 的diff 也不是免费
的他也会有性能上的损耗。
在优化页面的内容之前我们声明三个前提
-
提高FCP的时间其实就是在优化关键渲染路径
-
如果它是一个样式文件(CSS文件),浏览器就必须在渲染页面之前完全解析它(这就是为什么说CSS具有渲染阻碍性)
-
如果它是一个脚本文件(JavaScript文件),浏览器必须:停止解析,下载脚本,并运行它。只有在这之后,它才能继续解析,因为 JavaScript 脚本可以改变页面内容(特别是HTML)。(这就是为什么说JavaScript阻塞解析)
针对以上的用例测试,我们发现,无论我们怎么优化,框架本身的性能损耗是无法抹除的,我们唯一能做的就是让框架更早的去执行初始化,并且初始化更少的内容,可做的优化手段如下:
-
所有初始化用不到的js 文件全部走异步加载,也就是加上
defer
或者asnyc
,并且一些需要走cdn的第三方插件需要放在页面底部(因为放在顶部,他的解析会阻止html 的解析,从而影响css 等文件的下载,这也是雅虎军规
的一条) -
js 文件拆包,以vue-cli 为例,一般情况下我们可以通过cli的配置 splitChunks 做代码分割,将一些第三方的包走cdn,或者拆包。如果有路由的情况下将路由做拆包处理,保证每个路由只
加载当前路由对应的js代码
-
优化文件大小 减少字体包、css文件、以及js文件的大小(当然这些 脚手架默认都已经做了)
-
优化项目结构,每个组件的初始化都是有
性能损耗
的,在保证可维护性
的基础上,尽量减少初始化组件的加载数量 -
网络协议层面的优化,这个优化手段需要服务端配合纯前端已经无法达到,在现在
云服务器
盛行的时代,自家单位一般都会默认在云服务器中开启这些优化手段,比如开启gzip
,使用cdn
等等
其实说来说去,提高FCP 的核心理念只有两个 减少初始化视图内容
和 减少初始化下载资源大小。
LCP(Largest Contentful Paint)
顾名思义就是最大内容绘制
, 何时报告LCP,官方是这样说的
为了应对这种潜在的变化,浏览器会在绘制第一帧后立即分发一个largest-contentful-paint
类型的`PerformanceEntry`,用于识别最大内容元素。但是,在渲染后续帧之后,浏览器会在最大内容元素发生变化时分发另一个`PerformanceEntry`。
例如,在一个带有文本和首图的网页上,浏览器最初可能只渲染文本部分,并在此期间分发一个largest-contentful-paint
条目,其element
属性通常会引用一个<p>
或<h1>
。随后,一旦首图完成加载,浏览器就会分发第二个largest-contentful-paint
条目,其element
属性将引用<img>
。
需要注意的是,一个元素只有在渲染完成并且对用户可见后才能被视为最大内容元素。尚未加载的图像不会被视为"渲染完成"。在字体阻塞期使用网页字体的文本节点亦是如此。在这种情况下,较小的元素可能会被报告为最大内容元素,但一旦更大的元素完成渲染,就会通过另一个PerformanceEntry
对象进行报告。
其实用大白话解释就是,通常情况下,图片、视频以及大量文本绘制完成后
就会报告LCP
理解了这一点,的优化手段就明确了,尽量减少这些资源的大小就可以了,经过测试,减少首屏渲染的图片以及视频内容大小后,整体分数显著提高,提供一些优化方法:
-
本地图片可以使用在线压缩工具自己压缩 推荐tinypng.com
-
接口中附带图片,一般情况下单位中都有对应的oss或者cdn传参配置通过地址栏传参方式控制图片质量
-
图片懒加载
SpeedIndex(速度指数)
Speed Index
采用可视页面加载的视觉进度,计算内容绘制速度的总分。为此,首先需要能够计算在页面加载期间,各个时间点“完成”了多少部分。在WebPagetest中,通过捕获在浏览器中加载页面的视频并检查每个视频帧(在启用视频捕获的测试中,每秒10帧)来完成的,这个算法在下面有描述,但现在假设我们可以为每个视频帧分配一个完整的百分比(在每个帧下显示的数字)
以上是官方解释的计算方式,其实通俗的将,所谓速度指数就是衡量页面内容填充的速度
经过测试,跟LCP相同,图片以及视频内容对于SpeedIndex的影响巨大,所有优化方向,通之前一致,总的来说,只要提高LCP 以及FCP 的时间SpeedIndex 的时间就会有显著提高
不过需要注意的是,接口的速度也会影响SpeedIndex的时间,由于AJAX流行的今天,我们大多数的数据都是使用接口拉取。如果接口速度过慢,他就会影响你页面的初始渲染, 导致性能问题,所以,在做性能优化的同时,请求后端伙伴协助,也是性能优化的一个方案。
排查性能瓶颈
上述分析,根据三个指标提供了一些常规的优化手段,那么在这些优化手段中,有的你可以立马排查到,并且优化例如:
-
优化图像,优化字体大小
-
跟服务端配合利用浏览器缓存机制.启用cdn、启用gzip等
-
减少网络协议过程中的消耗,减少http 请求、减少dns查询、避免重定向
-
优化关键渲染路径,异步加载js等
但是有的优化手段我们不容易排查,因为他是打在包里面的,这个js 文件包含了很多逻辑怎么办,这里我有两个手段或许能够帮助排查出性能瓶颈发生在哪里:
分析包内容
在通常情况下,我们无法判断的优化点,都是在打包后,我们无法分析出,那些东西不是我们在首屏必须需要的,从而不能做出针对新的优化,为了解决当前问题,各大bundle厂商也都有各自的分析包的方案。
以vue-cli 为例
"report": "vue-cli-service build --report"
我们只需要在脚手架中提供以上命令,就能在打包时生成,整个包的分析文件
如上图所示 在打包后就能分析出打包后的js 文件他包含什么组件,如此以来,我们就能知道那些文件是没必要同步加载的,或者走cdn的,通过配置将他单独的隔离开来,从而找出性能的问题
利用chorme devtool 的代码覆盖率
如下图所示:
利用 devtool的代码覆盖率检查就能知道那些js 或者css 文件的代码没有被使用过,结合包内容的分析,我们就能大概的猜出性能的瓶颈在哪里从而做相应的特殊处理
最后
不要为了性能优化而性能优化,我们要因地制宜
,在不破坏项目可维护性的基础上去优化,千万不要你优化个项目性能是好了,但是大家都看不懂了,这就有点得不偿失了,还是那句话,60分万岁61份浪费,差不多得了
,把精力留着去干更重要的事情!