首页 > 服务市场 > 平台介绍 > TP90性能指标描述和优化建议

TP90性能指标描述和优化建议

TP90前端性能指标描述

前置说明
首先我们先来看前端性能很常见的一张图:

这张图是一个页面从打开到加载完成经历的各个阶段。
从navigatorStart到responseEnd这段时间算作服务器时间,即从页面开始加载到服务器响应返回内容这个阶段;
从responseEnd到loadEventEnd这段时间算作浏览器渲染的时间。
目前浏览器(低版本已无使用量)都提供了现成的API来获得各个阶段的时间耗时:performance.timing或者performance.getEntriesByType('navigation')[0]。
前者算作Timing标准,后者算作Timing2标准。
Timing目前浏览器已不再提供维护支持,Timing2获取的精度更加准确(比飞秒还精确,为10的-17次方秒),但兼容性尚且没有那么好(主要是iOS11才开始支持,其他浏览器都基本没问题了,IE9我们就让它见鬼去吧)。在京麦插件性能统计中,我们优先判断是否支持后者,除非后者不支持才使用前者。

性能指标定义及获取
接下来我们讲讲性能指标的标准。
前端性能指标划分:
• 白屏时间
从浏览器开始加载页面到首次出现内容之前的这段时间。
计算方式:
–Timing: performance.timing.responseEnd - performance.timing.navigationStart
–Timing2: performance.getEntriesByType('navigation')[0].responseStart
•首屏时间
从浏览器开始加载页面到屏幕可见区域的内容已经加载完成的时间。首屏时间浏览器目前并没有提供API来获取,不过有测速工具可以拿到首屏时间, 比如chrome控制台的performance页签功能,website,网上也有前端设计了获取首屏时间的方案,如微店刘远洋提出的一种打点方案,不过鉴于较消耗资源,京麦插件监控并没有获取首屏时间。
•可操作时间
从浏览器开始加载页面到JS执行完成(这时候dom事件可以认为已经绑定上了)的时间。
计算方式:
–Timing: performance.timing.domContentLoadedEventEnd - performance.timing.navigationStart
–Timing2: performance.getEntriesByType('navigation')[0].domContentLoadedEventEnd
网上也有人提出应该用domContentLoadedEventStart这个时间点进行计算,但目前大部分的框架、组件或者页面都是在domContentLoaded事件开始绑定交互事件(如jquery的$(function() {}),因此这里我决定使用domContentLoadedEventEnd这个时间点。(也有人习惯在load事件里绑定事件,这个不在考虑范围内)
•整页时间
从浏览器开始加载页面到整个页面加载完毕(最明显的标识就是移动端浏览器进度条读完了,pc端浏览器就是当前页签前面的loading消失了)。
计算方式:
–Timing: performance.timing.loadEventEnd - performance.timing.navigationStart
–Timing2: performance.getEntriesByType('navigation')[0].loadEventEnd
对于各个性能时间的获取,我们在load事件触发时,通过轮询判断loadEventEnd时间是否有了(因为需要等到laod结束才有这个时间),等有了这个时间才进行采集。(轮询为定时器轮询,为了不对页面原本性能造成影响,设置了250毫秒判断一次)
TP说明
什么是TP时间?即将所有数据进行从小到大(或从大到小)进行排序,然后取第百分之几的那个数据的值,这个通常在于分析大量数据的时候采用的指标,如TP90则是取从小到大(或从大到小)排序的第90%个数据。
在京麦插件中,我们以整页时间作为评定页面性能的指标,并且选用的是TP90这个指标(一般来说取得TP越高,则对数据越严格)。
页面加载流程
从页面发送一个页面URL请求出去,到服务器返回这个HTML的文本内容,这段时间也就是白屏时间,这里performance.getEntriesByType('navigation')[0]计算的时间只是这个HTML的,而对于其他资源的加载耗时是计算在页面的整页时间里(通过JS后发出去的请求不计算在内,比如ajax请求,JS动态插入一张图片或者jsonp请求插入一个script资源)。
当浏览器开始接收到服务器返回的文本时,它开始从上到下按顺序解析HTML文本,当遇到CSS、JavaScript、Image时,会发送出请求进行资源请求。
在发出请求的过程中,DOM继续解析,构建DOM树,但渲染需要等到CSS文件都请求回来,构建完CSSOM树后才开始渲染。
在这期间,如果遇到JS请求回来,则会阻塞页面的渲染,先执行完JS脚本,而JS脚本受CSS文件的阻塞,即需要等CSS执行完后,后面的JavaScript才可以执行。HTML的执行时从上到下顺序执行,如文件顺序为a.js -> a.css -> b.js,则a.js直接执行,而b.js就算先于a.css请求回来,也不会执行,需要等到a.css请求回来并执行完后才会执行JS。
:::info 常见的情况是,JS文件放在HTML前面部分,然后取获取DOM节点,但HTML还没解析到后面部分,所以DOM树上还没有这个DOM节点。 :::
在解析完HTML文本并构建完DOM树后,浏览器将进行首次Layout布局,构建document,等CSSOM构建完毕后,开始渲染(paint)页面,在这期间,渲染(paint)会执行多次,JS修改Dom结构也会触发渲染(paint)。
在JS的执行过程中,发送出去的Ajax请求不影响整页时间(即不会计算进整页时间),除非这个Ajax请求是同步请求。JS在页面加载过程中动态插入script请求(如jsonp),会影响整页时间。JS在页面加载过程中发送出去图片请求,也会计入整页时间中。如果页面加载完了,这时候再发出去的任何请求都不影响整页时间。
因为JS会阻塞dom的解析和渲染,所以这也是为什么要求将JS文件放在HTML末尾引用。
因为DOM树的渲染需要等到CSSOM树的构建才可以进行渲染,所以才要求将CSS文件放在HTML的头部,早点发送出去CSS文件的请求。
:::error 如果在HTML头部同时放了CSS代码(文件)和JS代码(文件),则因为CSS会阻塞JS的执行,而JS会阻塞HTML的解析,导致HTML会停止解析,需要等到CSS文件返回并执行后,JS代码再执行,等这些都执行完毕后才会继续执行HTML解析,这样会导致HTML后面的资源请求都卡住,不会发送(HTML不解析到这就不会发送请求),都得等前面的CSS文件和JS文件执行完后才能发送出去请求。(这样等于手动设置了个同步请求把页面性能给拖垮了)

________________________________________
:::info 目前京麦插件的性能统计的是访问插件的首页,即可能不是固定的URL。
对于单页面应用来说,不管访问的是哪个页面,算的都是同一个性能,因此这种页面问题不大。
而多页面应用,对于插件来说一般有启动入口,所以一般首页的URL基本是同一个,因此统计首页性能问题也不大。 :::
________________________________________
参考资料
•前端性能的几个基础指标
•蚂蚁金服如何把前端性能监控做到极致?
•css加载会造成阻塞吗?
•浏览器渲染原理及解剖浏览器内部工作原理


TP90前端性能优化分析

TP90前端性能优化分析

在上一篇《web前端性能指标描述》中,我们介绍了web页面性能的评判指标和计算规则,以及浏览器的渲染流程,接下来我们讲下怎么进行性能优化分析。
怎么定位性能问题
我们定位性能有问题有2种方式。
1种是通过用户的真实访问数据来查看一般用户访问网页的性能瓶颈在哪个问题,不过这种方式的定位很有限,并不能提供太多的帮助信息。
1种是通过性能调试工具进行调试,这种调试工具有多个,在这里我们主要讲下Chrome浏览器自带的performance功能和Chrome插件pagespeed这两个工具的使用。
为什么需要通过用户的真实访问数据来看性能呢,用性能调试工具不就好了吗? 性能调试工具是开发者在本地进行页面性能调试看的,并不能百分百还原用户的真实访问环境。我们的web页面是开发给用户用的,如果我本地调试性能没问题,基本在3秒内加载完毕,但用户真实反馈过来的却是各种卡顿,加载慢,要加载5,6秒,那我们本地调得再好又有什么用。(这里的前提是用户访问基数大,筛掉一些异常数据,这样的数据才可靠)
Chrome插件pagespeed使用
这个插件是一个性能评分插件,它可以指出你的页面的性能有哪些可以优化的地方。
安装完pagespeed之后,打开你要调试的网页,打开控制台的pagespeed,然后点击左上角的ANALYZE按钮,开始分析页面性能情况。

左边的颜色圆点是可以优化的程度:
•灰色:没有违背该规则
•绿色:违背规则,程度较轻,违背该规则的地方比较少
•橙色:违背规则,程度一般,违背该规则的地方数量一般
•红色:违背规则,程度严重,违背该规则的地方较多
接下来我们讲下图中各个优化建议。
尽量减少请求的数据量
这条规则是大部分网页都会违背的,一般在于资源请求数量较多和接口请求数量较多。
对于资源我们可以将一些更新频次很少的lib包(第三方包)进行合并,部分业务静态资源(JS、CSS)如果变化频次较低的话,也可以将其合并成1个资源。
对于接口请求,后端在做微服务进行接口拆分解耦,但在给前端提供接口的时候,可以尝试做一层接口聚合,将多个接口包装成1个接口(但会有木桶效应的问题,即聚合的接口的响应时间受聚合起来的那几个接口的最长响应时间的影响)
1个域名只能最多同时发送6个请求,其他请求只能等前面的请求结束后才能开始请求,因此尽量将1个域名的并发请求控制在6个或6个以下。
由同一网址提供资源
这个描述的是静态资源内容的合并,比如https://vmp.jd.com/dist/static/css/client.3d15167e.css和https://vmp.jd.com/dist/static/css/interface.3d15167e.css被判断为1个路径下的资源,可以进行合并。(用webpack4打包的资源的CSS都按照页面懒加载进行拆分了,这个目前暂时没有合并成1个css文件的配置)
使用浏览器缓存
这个一般是针对CDN服务器,对于一些不常更新的静态资源,谷歌是建议缓存一周或者一年。但对于我们这边的CDN设置,一般是设置1小时过期。
若有自己单独的CDN域名,如京麦的jm-static.jd.com,可以自己让运维修改过期时间,京麦的CDN服务器过期时间是1天。
如果需要更新静态资源,但还没到过期时间,可以上np.jd.com刷新CDN资源,让全国CDN节点主动拉取新的内容进行更新。
HTTP具体的缓存策略可以自己去研究下。
压缩Javascript / 压缩CSS / 压缩HTML
在传统的web开发模式中,并没有使用打包构建工具(如webpack、gulp),一般都是在jsp或者velocity中书写JS和CSS,就算有单独的JS文件拆分,也并不会进行混淆压缩。一般经过混淆压缩的静态资源能缩减50%的大小左右。
混淆压缩同时也能大大降低代码的可读性,这也是为什么前端现在更偏向于前后端分离开发,因为它用构建工具可以做一系列性能优化。
指定字符集
这个是指请求的响应头(Response Headers)中没有设置Content-Type,能加快浏览器解析运行,一般违背这条规则的情况并不常见。
优化图片
这条是指对图片开启Gzip压缩,目前我们用的大部分是图片系统的CDN服务,上面并没有开启Gzip压缩。
优化样式表和脚本的排列顺序 / 将CSS放在文档标头处(即head标签里) / 暂缓图片压缩
就跟上一篇文章中讲的,对于CSS和JS文件的排列顺序是有要求的,因为CSS会阻塞JS的执行,而JS会阻塞HTML的解析,严重影响页面加载。
应该将CSS文件放在head标签中,将JS文件放在body的最底下,避免阻塞HTML解析渲染。
将图片组合为CSS贴图定位
即将图片做成雪碧图(CSS Sprites),将多张图片合成一张图片,对图片的使用通过CSS背景图片定位进行设置。这种一般用在web页面中有多个icon图片的场景下。这个规则是减少请求。
目前雪碧图的方式已经很少使用了,目前大部分网页都是用的iconfont图标字体或者base64的方式。
将查询字符串从静态资源中删除
即在静态资源的地址最后面加上参数,如https://static.360buyimg.com/bus/web2017/bwb3.0/web/fonts/wbi.ttf?p9ayy4,一般加上参数是为了避免缓存的情况,但这样也就无法很好地利用缓存。
现在对于避免缓存一般是在打包静态资源的时候对资源的名字设置hash字符串(根据内容变化而变化的hash字符串,在打包构建工具中有提供),而且也可以通过HTTP缓存策略来避免缓存不刷新的情况,所以没必要在静态资源的地址上加参数来避免缓存。
尽量减少重定向
部分资源或者接口请求会进行重定向,部分接口是设置http重定向到https,这种可以直接修改请求的路径,避免请求到需要进行重定向的地址上。
指定图片的大小
这种是对于img标签而言的,对于确定大小的img标签,可以固定其宽高,避免浏览器进行重排重绘。
提供压缩后的图片
比如请求一张1000x500的图片,实际上它在页面上只显示100x50的大小,那么我们没必要去请求那么大的一张图片消耗带宽,通过设置图片系统中的图片地址的路径即可使用对应大小的图片,如原图片地址为:
https://img10.360buyimg.com/live/jfs/t1/35706/12/6312/1131936/5cc2c590Ecbb68a9e/93c0a83708162206.png
压缩200x100的地址为:
https://img10.360buyimg.com/live/s200x100_jfs/t1/35706/12/6312/1131936/5cc2c590Ecbb68a9e/93c0a83708162206.png
对于图片尺寸的问题,为了让图片在Retina屏幕上不至于模糊,建议还是使用网页上图片大小的2倍比例。
请指定一个“Vary: Accept-Encoding”标头
这个是针对使用Gzip压缩的资源,详细介绍见:HTTP协议中Vary的一些研究
请指定缓存校验工具
这种情况发生在base64的资源(图片、字体)上,使用base64的话则无法复用缓存,这个地方跟减少请求进行取舍,看哪种优化方式更高效。
Chrome调试工具performance的使用
打开Chrome的控制台(),切换到Performance页签,点击左上角的刷新按钮,等到加载完成后,将会出现一张资源请求的瀑布图:

通过资源加载瀑布图,我们可以查看是主要是哪些资源卡住了页面的整页时间。请求是什么时候发送出去的,什么时间结束的,都可以观察到。之后再根据哪个请求是瓶颈,对那个请求进行分析。
页面加载各阶段优化手段
从上一篇文章我们知道了页面加载总共分成这几个阶段:
重定向时间 -> DNS缓存查询时间 -> DNS查询时间 -> TCP连接时间 -> Request请求时间 -> Response请求响应时间 -> DOM解析时间 -> DOM渲染时间 -> Load事件执行时间
下面我们来看下各阶段的优化手段。
重定向时间
这个很简单,就是请求到正确的地址上即可。
经常会出现的情况是:
•将index.html的文件地址请求写成这样:https://jmw.jd.com/pc/versionUpdate,这样会导致浏览器重定向到https://jmw.jd.com/pc/versionUpdate/
•将http请求重定向到https上
这种情况注意下即可。
DNS缓存查询时间
这个是浏览器做的处理,如Chrome浏览器对DNS的缓存时间是1分钟。
这个阶段我们无法优化。
DNS查询时间
HTML的请求的DNS查询时间我们无法缩短,但对于里面的其他域名的请求,我们可以提前进行DNS查询,减少资源或者接口的请求时间。
在HTMl的头部head中加上DNS预查询即可。
<link rel="dns-prefetch" href="//example.com">
一般对CDN需要用到的域名做解析即可。以下是CDN的各个域名:
<link rel="dns-prefetch" href="//static.360buyimg.com">
<link rel="dns-prefetch" href="//img10.360buyimg.com">
<link rel="dns-prefetch" href="//img11.360buyimg.com">
<link rel="dns-prefetch" href="//img12.360buyimg.com">
<link rel="dns-prefetch" href="//img13.360buyimg.com">
<link rel="dns-prefetch" href="//img14.360buyimg.com">
<link rel="dns-prefetch" href="//img20.360buyimg.com">
<link rel="dns-prefetch" href="//img30.360buyimg.com">
<!-- 下面这个是京麦的CDN服务器,如果有用到京麦的JSSDK文件,可以把下面这条加上 -->
<link rel="dns-prefetch" href="//jm-static.jd.com">
TCP连接时间
TCP连接时间主要在3次握手中,缩短这个时间就是拉近用户和服务器机房的距离,对于接口的请求这个很难做到,但对于静态资源的TCP连接时间,我们可以通过CDN全国节点缩短用户与服务器之间的距离。
另外,我们还可以用HTTP2的keep-alive来保持长连接,这个CDN服务器默认是开启的,对于接口服务器也可以进行开启,虽然异步Ajax请求并不影响整页时间,但能让用户早点看到内容,何乐而不为。
Request请求时间 和 Response请求响应时间
在Request请求中,请求返回的时间取决于服务器的处理时间。
对于前后端耦合的项目,因为需要处理逻辑,拿出数据再动态构建HTML文本,之后再将HTML文本返回给浏览器,那这段时间主要在于处理逻辑上。
对于前后端分离的项目,因为返回的是一个空的HTML文本,数据都是等JS调用Ajax获取的,所以HTML的Request请求时间很短。但这样我们得处理Ajax请求,它也有Request请求的时间,这个也得看怎么在服务端进行性能提升。
:::warning 虽然Ajax请求不计算在整页时间中,但也别为了缩短整页时间而选择前后端分离,这个并不是它的优势所在。用户关心的是页面什么时候加载完,这其中包含数据什么时候展现,所以别为了整页时间的优化而优化,而应该关心用户体验,就算是Ajax接口也应该考虑怎么提升性能。
而且使用前后端分离其实会加长页面的白屏时间,这个也是一个衡量页面性能的一个重要指标。 :::
对于静态资源的请求,一般资源越小,请求越快(也受服务器带宽的影响)。前面说的不需要用那么大的图片,我们就使用小尺寸的图片,这种方式其实就是通过减小请求的大小从而来缩短请求响应时间。
DOM解析时间
这段时间从服务器响应返回HTML文本,浏览器开始按照从上到下的执行顺序解析HTML文本,到执行完HTML中的所有JS,这段时间我们将其称为DOM解析时间。所以优化这段时间在于缩短HTML中的CSS文件和JS文件的请求时间和执行时间,以及缩短HTML的解析时间。
•缩短CSS文件和JS文件的请求时间
这个可以从CDN、压缩文件(代码混淆压缩、Gzip)、文件拆分按需加载(单页面应用中的策略)等方式缩短加载时间,同时对于JS代码中,优化执行逻辑,避免耗时处理。
•缩短HTML的解析时间
再次强调一下:
1、CSS会阻塞HTMl的渲染和JS的执行
2、JS会阻塞HTML的解析和渲染

那么关键在于不要让JS阻塞HTML的解析,也不要让CSS阻塞到JS的执行。所以就严格遵守CSS文件放在head头部,JS文件放在body尾部。
另外就是减少DOM树的嵌套深度,这个属于代码层次的细节优化,一般在于代码书写习惯上,这个不好优化。
•DOM渲染时间
这个时间就在于减少重排(页面布局变动)和重绘(页面重新渲染元素样式)。
重排伴随着重绘,重绘一般发生在dom元素的颜色,透明度等非布局样式的变动上。
1、JS 中 CSS 属性读写分离
2、切换 class 或者 style.csstext 属性来批量修改样式
3、DOM 元素离线更新(使用appendChild和Document Fragment)
4、将没用的元素设为不可见:visibility: hidden
5、压缩 Dom 的深度,多使用伪元素或者 box-shadow
6、指定 img 标签的大小
7、使用独立渲染层(layer)
浏览器在渲染页面的时候,会构造多层渲染层(layer),你可以理解为PhotoShop中的图层。GPU会对渲染层进行缓存,对于会引起渲染层经常变动的元素,可以将它独立成一个渲染层。
触发新的渲染层的方式或元素:Video元素,WebGL,Canvas,CSS3 3D,CSS滤镜,z-index大于相邻节点。
Load事件执行时间
Load事件执行时间,这个时间的压缩就是减少onload回调函数的耗时处理。
很多人习惯将事件绑定放在onload事件中,其实是可以将其提前,放在尾部JS中直接执行或者DOMContentLoaded事件中,这时候DOM解析完成了,已经可以获取到DOM节点了。
jQuery中的$(function(){})函数其实就是对DOMContentLoaded的封装