以下内容均来自《高性能JavaScript》
管理浏览器中的JavaScript代码是一个棘手的问题,因为代码执行会阻塞浏览器,比如界面绘制。每次遇到<script>标签,浏览器都会停下来等待代码下载并执行,然偶再继续处理其他部分。我们可以通过如下几种方法来减少JavaScript文件对性能的影响
将所有<script>标签放置在页面的底部,紧靠body关闭标签</body>的上方。这样可以保证页面在脚本运行之前完成解析
将JS文件打包,页面的<script>标签越少,页面的加载速度越快,响应也越迅速。无论外部脚本文件还是内敛代码都是如此
在JavaScript中,数据存储位置可以对代码整体性能产生重要影响,有四种数据访问类型:直接量,变量,数组项,对象成员,这些访问方式性能不同
直接量和局部变量访问速度很快,而数组项和对象成员需要更长的时间
在函数内部访问变量时,会顺着作用域链向上查找,直到找到为止,这也意味着作用域链越长,平均的查找时间也就越长。而像with和try-catch会增加作用域链的长度,所以也会降低性能。由此可以得知,访问全局变量很慢,因为他们在作用域链的最后一环
访问对象的属性的时候,我们有时候会需要遍历原型链,这也意味着原型链越长,查找元素的平均效率就越低,变量所在的原型在原型链中越深,访问越慢
当浏览器下载完所有的HTML、JavaScript、CSS、图片之后,它会解析文件并创建两个内部数据结构:一棵DOM树和一棵渲染树
每个需要被显示的DOM树节点在渲染树中至少有一个节点(隐藏的DOM节点自然在渲染树中没有节点),渲染树上的点被称为框或者盒,根据CSS模型定义,将页面元素看做一个具有填充、边距、边框和位置的盒。一旦DOM树和渲染树构造完毕,浏览器就可以绘制页面上的元素了。
当DOM改变影响到元素的几何属性(宽和高),浏览器就需要重新计算元素的几何属性。如果这个元素的改变影响到其他元素,浏览器会使渲染树上收到影响的部分失效,然后重构渲染树。这就是回流(也叫重排版)。重排版完成时,浏览器在一个重绘进程中重新绘制屏幕上受影响的部分
当然并不是所有DOM改变都会影响到几何属性,比如改变背景颜色之类的,这种情况下就只会触发重绘
回流和重绘都是负担很重的操作,会导致浏览器阻塞,所以需要尽可能避免
因为计算量与每次回流有关,大多数浏览器会通过一个渲染队列来进行优化。但是用JavaScript获取一些DOM属性时,会(不由自主地)强迫队列中的所有渲染事件前不完成。比如获取如下属性: * offsetTop, offsetLeft, offsetWidth, offsetHeight * scrollTop, scrollLeft, scrollWidth, scrollHeight * clientTop, clientLeft, clientWidth, clientHeight * getComputedStyle() (在IE中为currentStyle)
为了让这些属性返回正确的值,浏览器不得不运行渲染队列中所有的渲染事件,这样才能保证值的正确。所以尽量减少这些属性的访问
多个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比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-中使用+和+=连接很慢,而使用Array.join()方式则快得多,这也是IE7-浏览器中唯一高效的连接大量字符串的途径
这种方法很慢,尽量不要使用
正则表达式处理经过如下几个步骤: 1. 编译 2. 设置起始位置 3. 匹配每个正则表达式的字元 4. 匹配成功或失败
正则表达式实现中,回溯是基本组成部分。它代价昂贵,且容易失控。回溯是正则表达式性能的唯一因素
正则表达式在匹配时,会在一个有多个分支的地方建立标记点,然后从左到右遍历所有的分支,如果分支符合,就会前进道下一个标记点,如果所有分支都不符合,就会回溯到上一个标记点,尝试上一标记点的其他分支。在尝试上一标记点的其它分支时,这一标记点如果需要,还会全部重新尝试
当一个正则表达式占用浏览器上秒,或者更长时间,很有肯那个就是回溯失控了,原因很有可能是出现了.*?这种非贪婪匹配,导致几乎每一个字符都会被作为标记点进行尝试
此类问题的解决办法是尽可能具体地指出分隔符之间的字符匹配形式,或者使用前瞻表达式
嵌套量词可能极大的加大分支的数量,比如一个正则表达式A+A+B显然没有AA+B好,比如匹配AAAAAAAAAAAAAAB时,前者产生的分支要比后者多得多
建议的一次JavaScript执行时间不超过100ms(最好在50ms)以内,可以通过setTimeout和setInterval来将任务进行分解,加入到UI线程中。其实这个思想和JavaScript引擎的垃圾回收器的迭代处理相似
AJAX获取数据时,可以使用POST或者GET方法
如果请求不改变服务器状态指示返回数据,应该使用GET。GET请求会被缓存,如果多次提取相同的数据会提高性能
而当URL和请求的参数长度超过2048个字符的时候才使用POST提取数据
多部分XHR允许使用一个HTTP请求获取多个资源,我们可以将资源打包成一个特定分隔符定界的大字符串,从服务器发送到客户端,JavaScript处理这些大字符串,然后根据它自身的类型和信息解析出每个资源
需要注意的是AJAX不会在浏览器中进行缓存,自然使用MXHR也不会缓存,在一些静态资源上使用这种方式其实并不太好。但是如果每次都确实需要去获取,分多个请求发送会更慢。
我们可以通过创建一个Image对象,将src设为一个脚本文件的URL,img元素我们并不需要插入到DOM中,这种形式称为IMG灯标
这种方式适用于GET请求,且服务器获得数据后不必返回数据给浏览器的情况
同时我们可以在Image的load事件中监听服务端是否成功接受了数据