前端性能优化(JavaScript补充篇)

正巧看到在送书,于是乎找了找自己博客上记录过的一些东西来及其无耻的蹭书了~~~

小广告:更多内容可以看我的博客

以下内容均来自《高性能JavaScript》

JavaScript文件加载

管理浏览器中的JavaScript代码是一个棘手的问题,因为代码执行会阻塞浏览器,比如界面绘制。每次遇到<script>标签,浏览器都会停下来等待代码下载并执行,然偶再继续处理其他部分。我们可以通过如下几种方法来减少JavaScript文件对性能的影响

将JS文件放在页面底部

将所有<script>标签放置在页面的底部,紧靠body关闭标签</body>的上方。这样可以保证页面在脚本运行之前完成解析

将JS文件打包

将JS文件打包,页面的<script>标签越少,页面的加载速度越快,响应也越迅速。无论外部脚本文件还是内敛代码都是如此

使用非阻塞方式下载

  1. <script>标签中添加defer属性
  2. 动态创建<script>元素,用它下载并执行代码
  3. 用XHR对象下载代码,并注入到页面中

数据访问

四种访问方式

在JavaScript中,数据存储位置可以对代码整体性能产生重要影响,有四种数据访问类型:直接量,变量,数组项,对象成员,这些访问方式性能不同

直接量和局部变量访问速度很快,而数组项和对象成员需要更长的时间

作用域链和闭包的访问速度

在函数内部访问变量时,会顺着作用域链向上查找,直到找到为止,这也意味着作用域链越长,平均的查找时间也就越长。而像with和try-catch会增加作用域链的长度,所以也会降低性能。由此可以得知,访问全局变量很慢,因为他们在作用域链的最后一环

原型链访问

访问对象的属性的时候,我们有时候会需要遍历原型链,这也意味着原型链越长,查找元素的平均效率就越低,变量所在的原型在原型链中越深,访问越慢

DOM

当浏览器下载完所有的HTML、JavaScript、CSS、图片之后,它会解析文件并创建两个内部数据结构:一棵DOM树和一棵渲染树

每个需要被显示的DOM树节点在渲染树中至少有一个节点(隐藏的DOM节点自然在渲染树中没有节点),渲染树上的点被称为框或者盒,根据CSS模型定义,将页面元素看做一个具有填充、边距、边框和位置的盒。一旦DOM树和渲染树构造完毕,浏览器就可以绘制页面上的元素了。

当DOM改变影响到元素的几何属性(宽和高),浏览器就需要重新计算元素的几何属性。如果这个元素的改变影响到其他元素,浏览器会使渲染树上收到影响的部分失效,然后重构渲染树。这就是回流(也叫重排版)。重排版完成时,浏览器在一个重绘进程中重新绘制屏幕上受影响的部分

当然并不是所有DOM改变都会影响到几何属性,比如改变背景颜色之类的,这种情况下就只会触发重绘

回流和重绘都是负担很重的操作,会导致浏览器阻塞,所以需要尽可能避免

何时触发回流

  1. 添加或删除可见的DOM元素
  2. 元素位置的改变
  3. 元素尺寸的改变(border、padding、margin、height、width)
  4. 内容改变
  5. 最初页面渲染
  6. 浏览器窗口改变尺寸

查询并刷新渲染树的改变

因为计算量与每次回流有关,大多数浏览器会通过一个渲染队列来进行优化。但是用JavaScript获取一些DOM属性时,会(不由自主地)强迫队列中的所有渲染事件前不完成。比如获取如下属性:
* offsetTop, offsetLeft, offsetWidth, offsetHeight
* scrollTop, scrollLeft, scrollWidth, scrollHeight
* clientTop, clientLeft, clientWidth, clientHeight
* getComputedStyle() (在IE中为currentStyle)

为了让这些属性返回正确的值,浏览器不得不运行渲染队列中所有的渲染事件,这样才能保证值的正确。所以尽量减少这些属性的访问

判断

if-else

多个if-else执行的时候,其会顺序检索每一个条件,直到所有条件检索完或检索到匹配的条件。所以我们可以通过树的形式组织if语句,如下面代码:

if(con === 1) {return result1;}
else if(con === 2) {return result2;}
else if(con === 3) {return result3;}
else if(con === 4) {return result4;}
else if(con === 5) {return result5;}
else if(con === 6) {return result6;}
else if(con === 7) {return result7;}
else if(con === 8) {return result8;}
else if(con === 9) {return result9;}

这段代码就会依次判断con是否等于1,2,3,4,5,6,7,8,9,如果是9的话,就会判断9次,我们可以将其改为:

if (con <= 3){
    if(con === 1) {return result1;}
    else if(con === 2) {return result2;}
    else {return result3;}
} else if(con > 3 && con <= 6){
    if(con === 4) {return result4;}
    else if(con === 5) {return result5;}
    else {return result6;}
} else if(con <= 9){
    if(con === 7) {return result7;}
    else if(con === 8) {return result8;}
    else {return result9;}
}

这样我们通过三叉树的形式,就可以减少查找次数了,比如这里查找9次,分别判断 0~3,3~6,6~9,7,8,9,只需要6次

if-else除了通过这种树形组织编码以外,还有一个优化的地方。由于其顺序判断的逻辑,我们可以根据概率来,将概率比较大的判断放在前面,概率较小的放在后面,这样也可以减少平均查找长度

switch

事实证明,大多数情况下switch比if-else更快,但是只有条件题数量很大的时候,才能明显更快。if-else在条件增加时,所带来的性能负担要高于switch,因此建议使用switch。不过switch只是用来判断几个不同的离散值,并没有if-else能判断离散值或值域那样的灵活性

打表法

可以使用打表的形式来做,把所有的处理函数放在一个数组中,然后将条件作为键,这种方法比switch和if-else都要快,而且在新增条件时,不会带来额外的性能开销

递归

很多算法都是递归实现,由于递归会多次触发函数调用,而函数调用也是需要开销的(比如创建运行期上下文、压运行期栈、创建AO、复制作用域链、销毁AO、弾栈等等),所以尽量将递归转变为循环。而运行期栈在很多浏览器中也有深度限制,当到达运行期栈的最大深度时,浏览器有各自的处理方式,但都是按照错误进行处理

字符串

不同的拼接方法

字符串拼接有很多不同的方法:
1. 使用+直接拼接
2. 使用+=拼接
3. 使用Array.join()拼接
4. 使用String.concat()拼接

使用加号拼接

使用++=时,不同的浏览器会做不同程度的优化,如果在IE7和他之前的浏览器,优化做的不好,比如如下操作:

str += "one" + "two"

实际上会执行如下步骤:
1. 内存中创建一个临时变量
2. 将这个临时变量赋值成"onetow"
3. 临时字符串与str拼接
4. 将结果赋予str

而如果改成如下这样:

str += "one"
str += "two"

这样就可以避免创建临时字符串,可一定程度加快性能

或者使用如下方式:

str = str + "one" + "two"

但是如果使用下面这种方式:

str = "one" + str + "two"

则无法确定是否有优化。不同的浏览器分配内存方式不一样,IE以外的浏览器,会尝试扩展表达式左端字符串的内存,然后简单的将第二个字符串拷到它的尾部,这样就会创建一个临时字符串存放one{str原本内容},导致性能降低

浏览器优化

很多浏览器会在编译时对连续相加的字符串进行拼接,以此来对运行时优化,比如:

str += "one" + "two"

会被优化成

str += "onetwo"

IE7-中的字符串连接

IE7-中使用++=连接很慢,而使用Array.join()方式则快得多,这也是IE7-浏览器中唯一高效的连接大量字符串的途径

String.concat

这种方法很慢,尽量不要使用

正则表达式优化

正则表达式的工作原理

正则表达式处理经过如下几个步骤:
1. 编译
2. 设置起始位置
3. 匹配每个正则表达式的字元
4. 匹配成功或失败

正则表达式实现中,回溯是基本组成部分。它代价昂贵,且容易失控。回溯是正则表达式性能的唯一因素

正则表达式在匹配时,会在一个有多个分支的地方建立标记点,然后从左到右遍历所有的分支,如果分支符合,就会前进道下一个标记点,如果所有分支都不符合,就会回溯到上一个标记点,尝试上一标记点的其他分支。在尝试上一标记点的其它分支时,这一标记点如果需要,还会全部重新尝试

回溯失控

当一个正则表达式占用浏览器上秒,或者更长时间,很有肯那个就是回溯失控了,原因很有可能是出现了.*?这种非贪婪匹配,导致几乎每一个字符都会被作为标记点进行尝试

此类问题的解决办法是尽可能具体地指出分隔符之间的字符匹配形式,或者使用前瞻表达式

嵌套量词导致性能下降

嵌套量词可能极大的加大分支的数量,比如一个正则表达式A+A+B显然没有AA+B好,比如匹配AAAAAAAAAAAAAAB时,前者产生的分支要比后者多得多

一些建议

  1. 关注如何让匹配更快失败:正则表达式慢往往不是因为成功慢,而是失败慢,因为失败会查找所有的情况
  2. 以简单的,必须的字元开始:这样可以加快失败的匹配,如果这个开始字元都不匹配,后面的标记点就不会被匹配了
  3. 编写两次模板,使他们后面的资源互相排斥:当字元与邻近的子表达式能够重叠匹配时,路径将显著正价,所以需要将其具体化
  4. 减少分之数量,缩小它们的范围:直接使用正则表达式中已有的类(如\w,\d)比使用|要快
  5. 使用非捕获分组:捕获分组花费时间和内存用于记录后向引用,而使用后非捕获性分组则避免这种开销
  6. 将正则表达式分层,先捕获感兴趣的文字,然后再使用新的正则表达式处理
  7. 暴露所需的字元,尽量简单地判断出那些必须的字元
  8. 使用适当的量词,贪婪量词和懒惰量词在匹配同样字符串时过程是不同的,在确保正确的前提下,选择回溯次数更少的量词可以提高性能
  9. 将正则表达式赋给变量,以重用他们。正则表达式创建时,需要对他们进行编译,这个编译也会有额外的开销
  10. 将复杂的正则表达式拆分成简单的片段,避免使用一个表达式做太多的工作,可以通过两个或多个正则表达式来解决

UI线程相关

建议的一次JavaScript执行时间不超过100ms(最好在50ms)以内,可以通过setTimeout和setInterval来将任务进行分解,加入到UI线程中。其实这个思想和JavaScript引擎的垃圾回收器的迭代处理相似

AJAX

AJAX获取数据时,可以使用POST或者GET方法

如果请求不改变服务器状态指示返回数据,应该使用GET。GET请求会被缓存,如果多次提取相同的数据会提高性能

而当URL和请求的参数长度超过2048个字符的时候才使用POST提取数据

多部分XHR(MXHR)

多部分XHR允许使用一个HTTP请求获取多个资源,我们可以将资源打包成一个特定分隔符定界的大字符串,从服务器发送到客户端,JavaScript处理这些大字符串,然后根据它自身的类型和信息解析出每个资源

需要注意的是AJAX不会在浏览器中进行缓存,自然使用MXHR也不会缓存,在一些静态资源上使用这种方式其实并不太好。但是如果每次都确实需要去获取,分多个请求发送会更慢。

IMG灯标

我们可以通过创建一个Image对象,将src设为一个脚本文件的URL,img元素我们并不需要插入到DOM中,这种形式称为IMG灯标

这种方式适用于GET请求,且服务器获得数据后不必返回数据给浏览器的情况

同时我们可以在Image的load事件中监听服务端是否成功接受了数据

数据格式

  • XML:支持广泛但解析效率低,而且相当冗长
  • JSON: 小巧,轻便,解析速度较快
  • JSONP:使用JavaScript解释器进行解析,解析速度极快,数据量只比JSON多一点点(函数名称和括号)
  • HTML:无需解析,数据冗长
  • 自定义格式:自己解析,慢而易出错,数据长度可以很短

其他

  • 不要使用eval以及其类似
  • 使用字面量
  • 避免重复工作,检测浏览器时,保存首次检测结果即可
  • 考虑位操作
  • 使用原声方法,因为他们是C++写的
  • 文件预处理,压缩(gzip)、合并、uglify
  • 尝试CDN
阅读 4.6k

推荐阅读
说学逗唱
用户专栏

202 人关注
24 篇文章
专栏主页