本文原创:gaoruixia
一. 开篇
提起前端性能优化,大家会想到哪些内容呢?
相信不同的人给出的答案不同,关于性能优化没有标准答案,它更像是一个摸索的过程,本篇文章给出从页面加载的各个过程分析可优化的点
二. 常用性能优化指标
下面这张图是一个页面从打开到加载完成经历的各个阶段
-
浏览器获得各个阶段的时间耗时的API
- Timing标准:window.performance.timing - Timing2标准: window.performance.getEntriesByType('navigation')[0]。
Timing目前浏览器已不再提供维护支持,Timing2获取的精度更加准确(比飞秒还精确,为10的-17次方秒),但兼容性尚且没有那么好(主要是iOS11才开始支持,其他浏览器都基本没问题了,IE9我们就让它见鬼去吧)。在京麦插件性能统计中,我们优先判断是否支持后者,除非后者不支持才使用前者。
-
白屏时间
从navigatorStart到responseEnd这段时间算作服务器时间
从浏览器开始加载页面到首次出现内容之前的这段时间(从页面发送一个页面URL请求出去,到服务器返回这个HTML的文本内容)计算方式 - Timing: performance.timing.responseEnd - performance.timing.navigationStart - Timing2: performance.getEntriesByType('navigation')[0].responseStart
-
浏览器渲染时间
从responseEnd到loadEventEnd这段时间,包括CSS、JavaScript、Image等资源的加载耗时
计算方式 - Timing: performance.timing.loadEventEnd - performance.timing.responseEnd - Timing2: performance.getEntriesByType('navigation')[0].loadEventEnd - performance.getEntriesByType('navigation')[0].responseStart
-
整页时间
从浏览器开始加载页面到整个页面加载完毕(最明显的标识就是移动端浏览器进度条读完了,pc端浏览器就是当前页签前面的loading消失了)
计算方式 - Timing: performance.timing.loadEventEnd - performance.timing.navigationStart - Timing2: performance.getEntriesByType('navigation')[0].loadEventEnd
使用Chrome调试工具performance查看浏览器请求一个页面到渲染完成的过程
可以打开浏览器控制台,切换到Performance,点击刷新按钮,等页面加载完成
三. 影响整页时间的因素
先了解下得到html文本内容后页面的加载渲染流程(以webkit主流程为例)
-
渲染流程有四个主要步骤
- 解析HTML生成DOM树 - 渲染引擎首先解析HTML文档,生成DOM树
- 构建Render树 - 接下来不管是内联式,外联式还是嵌入式引入的CSS样式会被解析生成CSSOM树,根据DOM树与CSSOM树生成另外一棵用于渲染的树-渲染树(Render tree),
- 布局Render树 - 然后对渲染树的每个节点进行布局处理,确定其在屏幕上的显示位置
- 绘制Render树 - 最后遍历渲染树并用UI后端层将每一个节点绘制出来
以上步骤是一个渐进的过程,为了提高用户体验,渲染引擎试图尽可能快的把结果显示给最终用户。它不会等到所有HTML都被解析完才创建并布局渲染树。它会在从网络层获取文档内容的同时把已经接收到的局部内容先展示出来。
-
阻塞渲染的因素
没有js的理想情况下,html与css会并行解析,分别生成DOM与CSSOM,然后合并成Render Tree,进入Rendering Pipeline;
但如果有js,css加载会阻塞后面js语句的执行,而(同步)js脚本执行会阻塞其后的DOM解析(所以通常会把css放在头部,js放在body尾)-
CSS的阻塞情况
- css不会阻塞DOM树的解析
- css会阻塞DOM树的渲染
- css会阻塞后面js语句的执行
-
JS的阻塞情况
- JS阻塞DOM解析,但浏览器会"偷看"DOM,预先下载相关资源
- 浏览器遇到script且没有defer或async属性的标签时,会触发页面渲染,因而如果前面CSS资源尚未加载完毕时,浏览器会等待它加载完毕在执行脚本
-
蓝色线代表网络读取,红色线代表执行时间,这俩都是针对脚本的;绿色线代表HTML解析
-
由上可以得出影响整页时间的因素
- CSS、JS、图片等资源加载
- CSS、JS文件放置位置,避免阻塞DOM解析和渲染
- ajax同步请求
- jsonp请求
- 页面加载过程中JS发送图片请求
四. 优化分析
使用Chrome调试工具performance
下图中,在NetWork资源加载瀑布图中我们可以查看是主要是哪些资源卡住了页面的整页时间,发送结束时间都可以观察到。之后再根据哪个请求是瓶颈,对那个请求进行分析
使用Chrome插件pagespeed和Lighthouse
-
使用插件pagespeed
首先去安装,安装完pagespeed之后,打开你要调试的网页,打开控制台的pagespeed,然后点击左上角的ANALYZE按钮,开始分析页面性能情况
插件Lighthouse类似,这里不详细介绍了
对于整页时间较长的情况,需要分析具体是在哪个阶段导致整页时间比较长,可以从上面整页时间的说明里的各个阶段去分析
整页时间主要在dom解析耗时上,这段时间是从HTML开始解析,到JS全部执行完毕,这之中包含了CSS、JS等资源的加载,需要去优化HTML解析这段时间。
五. 各阶段优化手段
从常用性能优化指标我们知道了页面加载总共分成下面这几个阶段:
重定向时间 -> DNS缓存查询时间 -> DNS查询时间 -> TCP连接时间 -> Request请求时间 -> Response请求响应时间 -> DOM解析时间 -> DOM渲染时间 -> Load事件执行时间
下面来看下各阶段的优化手段
1. 重定向时间
这个很简单,就是请求到正确的地址上即可。
经常会出现的情况是:
- 将index.html的文件地址请求写成这样:
https://xx.com/xx
,这样会导致浏览器重定向到https://xx.com/xx/
- 将http请求重定向到https上
这种情况注意下即可
2. DNS缓存查询时间
这个是浏览器做的处理,如Chrome浏览器对DNS的缓存时间是1分钟。
这个阶段我们无法优化。
3. DNS查询时间
HTML的请求的DNS查询时间我们无法缩短,但对于里面的其他域名的请求,我们可以提前进行DNS查询,减少资源或者接口的请求时间。
在HTMl的头部head中加上DNS预查询即可
一般对CDN需要用到的域名做解析即可。以下是CDN的各个域名:
4. TCP连接时间
TCP连接时间主要在3次握手中,缩短这个时间就是拉近用户和服务器机房的距离,对于接口的请求这个很难做到,但对于静态资源的TCP连接时间,我们可以通过CDN全国节点缩短用户与服务器之间的距离。
另外,我们还可以用HTTP2的keep-alive来保持长连接,这个CDN服务器默认是开启的,对于接口服务器也可以进行开启,虽然异步Ajax请求并不影响整页时间,但能让用户早点看到内容,何乐而不为。
5. Request请求时间 和 Response请求响应时间
在Request请求中,请求返回的时间取决于服务器的处理时间。
对于前后端耦合的项目或者SSR的项目,因为需要处理逻辑,拿出数据再动态构建HTML文本,之后再将HTML文本返回给浏览器,那这段时间主要在于处理逻辑上。
对于前后端分离的项目,因为返回的是一个空的HTML文本,数据都是等JS调用Ajax获取的,所以HTML的Request请求时间很短。但这样我们得处理Ajax请求,它也有Request请求的时间,这个也得看怎么在服务端进行性能提升。
虽然Ajax请求不计算在整页时间中,但也别为了缩短整页时间而选择前后端分离,这个并不是它的优势所在。用户关心的是页面什么时候加载完,这其中包含数据什么时候展现,所以别为了整页时间的优化而优化,而应该关心用户体验,就算是Ajax接口也应该考虑怎么提升性能。
而且使用前后端分离其实会加长页面的白屏时间,这个也是一个衡量页面性能的一个重要指标,白屏时间可以通过添加骨架屏来优化。
对于静态资源的请求,一般资源越小,请求越快(也受服务器带宽的影响)。可以通过减小静态资源(JS、CSS、图片等)请求的大小从而来缩短请求响应时间。
6. DOM解析时间
这段时间从服务器响应返回HTML文本,浏览器开始按照从上到下的执行顺序解析HTML文本,到执行完HTML中的所有JS,这段时间我们将其称为DOM解析时间。所以优化这段时间在于缩短HTML中的CSS文件和JS文件的请求时间和执行时间,以及缩短HTML的解析时间。
缩短CSS文件和JS文件的请求时间
可以从以下几方面来做
- 静态资源放在CDN上
- 压缩文件(代码混淆压缩,Gzip)
- js文件拆分按需加载(webpack合理配置, 可使用webpack-bundle-analyzer来分析打包)
缩短HTML的解析时间
- 从前面影响整页时间的因素中我们可以知道,CSS会阻塞HTMl的渲染和JS的执行,JS会阻塞HTML的解析和渲染,因此为了避免阻塞,要严格遵守CSS文件放在head头部,JS文件放在body尾部
- 减少DOM树的嵌套深度,这个属于代码层次的细节优化,一般在于代码书写习惯上,这个不好优化
- 优化JS代码执行逻辑,避免耗时处理,必要时可以使用Web Worker开启多线程
7. DOM渲染时间
这个时间就在于减少重排(页面布局变动)和重绘(页面重新渲染元素样式)
减少重排和重绘的手段
- JS 中 CSS 属性读写分离
- 切换 class 或者 style.csstext 属性来批量修改样式
- 将没用的元素设为不可见
- 压缩 Dom 的深度,多使用伪元素或者 box-shadow
- 指定 img 标签的大小
- 使用独立渲染层(触发新的渲染层的方式或元素:Video元素,WebGL,Canvas,CSS3 3D,CSS滤镜,z-index大于相邻节点)
- DOM 元素离线更新(使用appendChild和Document Fragment)
8. Load事件执行时间
这个时间的压缩就是减少onload回调函数的耗时处理
很多人习惯将事件绑定放在onload事件中,其实是可以将其提前,放在尾部JS中直接执行或者DOMContentLoaded事件中,这时候DOM解析完成了,已经可以获取到DOM节点了
总结
本文从页面加载的各个阶段分析影响因素,简单的给出了可优化的方向,具体实施还需要在工作中结合团队现状来进行