前端性能的一些小总结

前端性能优化,老生常谈的问题了,提到性能,可以说的太多了,想真正的对性能进行优化,必须深入了解!

1.想深层次探究前端优化,就不仅仅停留到表面,要知道文件如何下载?浏览器如何渲染?

  • 想优化页面,就要知道一个页面,产生的整个过程

浏览器产生一个页面的流程如下:

此处输入图片的描述
>

1.1下载:最有优化点的一部

下载,这个方向是优化必要点,贴一张chrome下载文件的时间截图:
此处输入图片的描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//来个表格,细致分析下下载过程中每个阶段的耗时
1.持续时间 = 时间 *个数
2.时间 = initial(浏览器初始化,准备数据)+request(请求到服务端) + serverProcess (服务处理数据)+response(返回数据,接受第一个字符到接受完最后一个字符的时间)
3. initial = queue(处理队列任务) + DNS解析 + tcp协议 +ssl协议
4. dns = delay*2(小体量信息,耗时大概是两个网络延时)
5. tcp = delay*3 ( 三次握手)
6. ssl = delay*5 (是一种文件安全协议,需要5次握手)
7. request = delay + size/brandwidth(带宽)
8. response = delay + size/brandwidth(带宽)

分析出:下载性能决定的几个因素:

  • 增加带宽
  • 减少资源排队
  • 减少网络延迟
  • 减少网络延迟影响
  • 控制资源总大小

下面逐个分析下:

1.1.1.增加带宽:
这个在用户端我们是无法控制的,难道公司出钱给大家都上一个百兆光纤?还有一个情况你会发现,在20M和 50M的带宽下,打开一个页面或者文件速度差不多,这就是因为带宽达到一个级别后,影响下载的速度的主要优化点就是其他方向了。
所以增加带宽,我们就在自己公司的网络上搞高点,用户端无能为力了。

1.1.2.减少资源排队:

  • 浏览器有“单域名”最大并行请求数量的限制
    ie7 - 2个
    ie8 - 6个
    ie10 - 8个
    chrome - 6个
    什么360浏览器好像比较变态,并行十几个。
    建议使用时,并行下载数折中取 4个,来处理。

  • 分域名追求更高的并行度
    把什么资源放到不同的域名下面,通常将静态资源分布在几个不同域,保证资源最完美地分域名存储,以提供最大并行度,让客户端加载静态资源更为迅速。
  • 分域名追求更高的并行度,与域名收敛取舍
    分域名在PC上是比较可行的,因为网络环境较好,但是在移动端,多个分域名,就会造成多个DNS解析,造成很大的时间开销浪费在DNS解析上。
    1
    2
    3
    4
    5
    6
    7
    8
    首先要知道,使用一个http请求去请求一个资源时,会经历些什么。简单而言:
    1、DNS 域名解析 -->
    2、发起 TCP 的 3 次握手 -->
    3、建立 TCP 连接后发起 http 请求 -->
    4、服务器响应 http 请求
    5、......略
    在这里第一步,也是关键的第一步 DNS 解析,在移动端的 http 请求耗时中,DNS 解析占据了大部分时间。

注:所以说,资源分域名和域名收敛要自行折中


  • 过多并行请求会造成Keep-Alive无效,进而增加网络延迟影响
    Keep-Alive是什么?
    在http早期,每个http请求都要求打开一个tpcsocket连接,并且使用一次之后就断开这个tcp连接。
    使用keep-alive可以改善这种状态,即在一次TCP连接中可以持续发送多份数据而不会断开连接。通过使用keep-alive机制,可以减少tcp连接建立次数,以此提高性能和提高http服务器的吞吐率(更少的tcp连接意味着更少的耗时。

1.1.3.减少网络延迟及影响:

  • 减少DNS查询(与分域名冲突,要找最优点)

  • 减少DNS等待
    1.空请求DNS Prefetch,如下面代码

    1
    2
    3
    4
    5
    6
    7
    8
    1. 用meta信息来告知浏览器, 当前页面要做DNS预解析:<meta http-equiv="x-dns-prefetch-control" content="on" />
    2. 在页面header中使用link标签来强制对DNS预解析: <link rel="dns-prefetch" href="http://bdimg.share.baidu.com" />
    <meta http-equiv="x-dns-prefetch-control" content="on" />
    <link rel="dns-prefetch" href="http://nsclick.baidu.com" />
    <link rel="dns-prefetch" href="http://hm.baidu.com" />
    <link rel="dns-prefetch" href="http://eiv.baidu.com" />
  • 减少TCP链接
    1.Keep-Alive应用
    2.合并资源(就是打包咯。。)

  • 让服务器距离用户更近

    1. CDN加速走起。。。

1.1.4.控制资源提炼:

  • 资源体量
    1.压缩、去注释

  • Cookie 大小
    1.分情况用Storage代替Cookie
    2.无Cookie需求独立CookieFree域名(一些静态资源,放到Cookie Free域名下,设置不传Cookie)


1.1.5.合并资源:
原理
1.减少了tcp握手消耗
2.要最大并行请求数量限制(并不是所有的东西都打包到一个文件里最好,要充分利用浏览器并行多个文件下载的特性)

?合并资源存在的问题:
1.脚本/样式解析被延迟(文件过大,解析当然延迟了)
2.存在带入无用内容的概率(多个文件打包在一起,可能存在此时还不需要的代码)
3.版本更新被绑在一起(有一个小的文件改动,build时就要更改版本,缓存被破坏的概率更高)

因此要从几个方便寻找合并文件的平衡点:

1
2
3
4
5
6
7
//一下建议有一部分是书面所得,并未实践
1.文件大小(建议单个打包文件不超过 500KB)
2.更新频率(有的文件更新频率过高,就尽量不打包进来,造成破坏整个文件缓存)
3.特别大的库单独拥有自己的一个脚本,比如ECharts之类的图表。
4.UI控件库,包含基础UI控件和业务UI控件,合并为一个脚本。
5.MVC框架、页面基类、工具类、系统通用功能层等业务无关的逻辑合并为一个脚本。
6.一级页面的业务模块合并为一个脚本。

1.1.6.缓存:
Cache缓存:(通过设置HTTP Header 参数Cache-Control实现)

1
2
1. 静态资源版本号+永久缓存是基本
2. 动态/入口资源短时间缓存有积极作用(比如商品的详情页缓存个几秒还是应该可以的)

Last-Modified & If-Modified-Since(也就是咱们常见的304)

localStorage 缓存数据
HTML 的localStorage是是本地存储的新玩意,适当的时候可以代替cookie,但是注意兼容问题和体积上限(大致5M左右)

注:上面是大致对‘下载这一过程的总结’


1.2解析:

1
2
3
4
解析页面:
1.解析是需要时间的,虽然很短
2.解析发生在主线程,会阻塞其它代码执行
3.解析和下载发生在不同线程,可并行HTML可使用http协议设置 Transfer-Encoding:chunk实现下载和解析并行

此处输入图片的描述

上图中,上面的是大部分网站的形式,下面这种,边下载边解析是比较少见的,上下对比起来,还是有一定的优化控件。

目前下面的这种形式Facebook有实践,大家可以搜索一下“BigPipe技术”,貌似淘宝也有一种“BigRender”的技术。(具体实现不详述了,感兴趣可以搜索下)


1.3样式匹配:

1.基本无法成为瓶颈
2.样式匹配自右向左进行
3.注意避免统配选择器(*)

注:个人感觉一般样式选择器对性能影响不大,好的书写是有利于代码的维护。


1.4布局方面的优化(有优化点):

1.4.1. 浏览器repaint(重绘) & relayout(排版)

不同的浏览器的渲染过程存在些许不同,但大体的机制是一样的,下图展示的是浏览器自下载完全部的代码后的大致流程
此处输入图片的描述

1
2
3
4
5
6
7
1.HTML代码转化成DOM
2.CSS代码转化成CSSOM(CSS Object Model)
3.结合DOM和CSSOM,生成一棵渲染树(包含每个节点的视觉信息)
4.生成布局(layout),即将所有渲染树的所有节点进行平面合成
5.将布局绘制(paint)在屏幕上
//这五步里面,第一步到第三步都非常快,耗时的是第四步和第五步

注意: 重绘和重排的性能消耗是非常严重的,破坏用户体验造成UI卡顿。

什么时候回触发重新渲染?

1
2
3
4
5
6
7
8
1.增加、删除、更新DOM节点;
2.通过display:none隐藏节点会触发重绘和回流,通过visibility:hidden隐藏只会触发重绘,因为没有几何结构的改变;
3.移动节点和动画;
4.增加、调整样式;
5.用户操作行为,如调整窗口大小、改变字体大小、滚动窗口(OMG,no!)等。
//重新渲染,就需要重新生成布局和重新绘制。前者叫做"重排"(reflow),后者叫做"重绘"(repaint)。
//注意点:"重绘"不一定需要"重排",比如改变某个网页元素的颜色,就只会触发"重绘",不会触发"重排",因为布局没有改变。但是,"重排"必然导致"重绘",比如改变一个网页元素的位置,就会同时触发"重排"和"重绘",因为布局改变了。

现在浏览器变得很聪明了,会智能合并多次重绘动作,如:

1
2
3
div.style.width = 12px;
div.style.color = #000;
这两行代码,只会触发一次重排和重绘

但是有时候特别书写时就不行,如下:

1
2
3
4
5
div.style.color = 'blue';
var margin = parseInt(div.style.marginTop);
div.style.marginTop = (margin + 10) + 'px';
//上面代码对div元素设置背景色以后,第二行要求浏览器给出该元素的位置,所以浏览器不得不立即重排。

所以,从性能角度考虑,尽量不要把读操作和写操作,放在一个语句里面。

1
2
3
4
5
6
7
8
9
10
11
12
13
// bad
div.style.left = div.offsetLeft + 10 + "px";
div.style.top = div.offsetTop + 10 + "px";
// good
var left = div.offsetLeft;
var top = div.offsetTop;
div.style.left = left + 10 + "px";
div.style.top = top + 10 + "px";
一般的规则:
1.样式表越简单,重排和重绘就越快。
2.重排和重绘的DOM元素层级越高,成本就越高。

总结重绘和重排的几个优化点:(尽量少的触发重排)

1
2
3
4
5
6
7
8
9
10
11
12
13
1.DOM 的多个读操作(或多个写操作),应该放在一起。不要两个读操作之间,加入一个写操作。
2.如果某个样式是通过重排得到的,那么最好缓存结果。避免下一次用到的时候,浏览器又要重排。
3.不要一条条地改变样式,而要通过改变class,或者csstext属性,一次性地改变样式。
4.先将元素设为display:none(需要1次重排和重绘),然后对这个节点进行100次操作,最后再恢复显示(需要1次重排和重绘)。这样一来,你就用两次重新渲染,取代了可能高达100次的重新渲染。
5.position属性为absolute或fixed的元素,重排的开销会比较小,因为不用考虑它对其他元素的影响。
6.使用虚拟DOM的脚本库,比如React等。
7.dom动画什么的,不要通过修改DOM位置进行,可以用transform等css属性,只触发重绘


一个页面的性能体验可以总结一下几点:

  • 对性能进行分类
    • 首屏时间
    • 绘制帧数
    • 交互响应时间
  • 时间的大致标准

    • 用户交互反馈100ms以上有延迟感(点个按钮100ms没反应)
    • 绘制16ms以上有掉帧感 (网上查的,对动画不是太了解)
    • 任务执行1s以上有不耐烦感(一个loading超过1s)
  • 当一个页面动画很卡时,干脆砍了,用其他方案代替


作者 [@sha Qihe]
2016 年 3月 20日
QQ:330630770