一,脚本的加载和执行
1,脚本影响性能的原因
- 当浏览器在执行
javascript代码时,不能同时做其他事情。多数浏览器使用单一进程来处理用户界面(UI)刷新和javascript脚本执行,所以同一时刻只能做一件事情。javascript执行过程耗时越久,浏览器等待响应的时间就越长。 - 每当遇到
<script>标签时,页面的下载和渲染都必须停下来等待脚本的解析和执行。因为脚本执行过程中可能会修改页面内容。 css下载不会阻塞页面的解析(dom tree),但是会阻塞页面的渲染(render tree)。同时当<link>后面紧跟着内嵌脚本时,由于为了确保内嵌脚本在执行时获得更精准的样式信息,此时的css下载会阻塞页面的解析。
2,优化技巧
- 大多数浏览器允许多个
js文件并行下载,<script>标签在下载外部资源时不会阻塞其他<script>标签的下载,但是仍然会阻塞其他资源的下载。 - 为了让用户尽早看到大部分页面,应该将脚本放在
body底部。 - 下载单个的
js代码文件虽然只产生一次HTTP请求,但是会锁死浏览器一大段时间,此时也可以用异步加载脚本来解决这一问题。 - 由于
HTTP请求会带来额外的性能开销,所以可通过离线的打包工具将多个js文件合并为一个。
二,数据存取
- 在执行环境的作用域链中,一个标志符所在的位置越深,它的读写速度就越慢。所以函数中读写局部变量总是最快的,读写全局变量总是最慢的,所以常用的全局变量可以存储在局部变量中。
- 在函数中,如果要多次读取同一个对象属性,最佳做法是将属性值保存到局部变量中。避免了多次查找带来的性能开销。
三,DOM操作
1,优化技巧
-
减少访问
DOM的次数,把运算尽量留在ECMAScript这一端处理。 -
textContent通常比innerHTML具有更好的性能,因为文本不会被解析为HTML。并且可以防止跨站脚本攻击XSS攻击。 -
使用
element.cloneNode(false) (element表示已有节点)替代document.createElement()。在将此复制的节点插入到文档前记得修改相应的属性值。 -
处理
HTML集合时,使用局部变量来存储需要多次读取的元素。将集合的长度也缓存到一个变量中,并在迭代中使用它。 -
需要从一个
DOM元素开始,操作其周围的元素时,或它的所有子节点,推荐使用element.firstElementChild()和element.nextElementSibling()。 -
使用
element.querySelector()和element.querySelectorAll()来获取元素。因为它们速度更快。 -
减少重绘和重排。
-
在元素很多时避免使用
:hover。比如很大的表格或很长的列表。 -
由于绑定事件处理器也是有代价的,所以可以使用事件委托来处理
DOM事件。在事件处理函数中访问事件对象,并判断事件源即可。
2,重绘与重排的定义
- 当
dom的变化影响了元素的几何属性(宽和高),浏览器需要重新计算元素的几何属性,同样其他元素的几何属性和位置也会因此受到影响。浏览器会使渲染树中受到影响的部分失效,并重新构造渲染树,这一个过程称为重排。 - 完成重排后,浏览器会重新绘制受影响的部分到屏幕中,该过程称为重绘。
3,对重绘和重排的优化
- 会发生重排的情况:添加或删除可见的
dom元素;元素位置改变;元素尺寸改变(包括外边距,内边距,边框厚度,宽度,高度等属性改变);内容改变,例如:文本改变或图片被另一个不同尺寸的图片替代;页面渲染器初始化;浏览器窗口尺寸改变 - 会导致队列强制刷新引起重排的方法(尽量避免使用):(实在要使用的话,可以将他们赋值给一个局部变量,然后再操作局部变量,避免多次获取)
offsetTop, offsetLeft, offsetWidth, offsetHeight, scrollTop, scrollLeft, scrollWidth, scrollHeight, clientTop, clientLeft, clientWidth, clientHeight, getComputedStyle() - 合并多次对dom和样式的修改,然后一次处理掉,最小化重排次数。对于样式,可以使用
element.style.cssText来修改内联样式,或使用element.className来修改样式。 对于dom,则可以通过文档片段来添加document.createDocumentFragment()。 - 对于展开/折叠等动画可以如下操作:使用绝对位置定位页面上的动画元素,使其脱离文档流。当动画结束时恢复定位。
四,算法和流程控制
1,循环
-
除非你明确需要迭代一个属性数量未知的对象,否则应避免使用
for-in循环。for-in循环每次循环都会同时搜索实例和原型属性,所以产生更多的开销,所以比其他循环(for, while, do-while)要慢。 -
减少迭代的工作量:比如减少对象成员及数组项的查找次数,采用倒叙循环:
for(let i = items.lenght; i--; ) { 循环体 } -
forEach()循环要比标准for循环慢。
2,条件语句
- 条件数量较少时用
if-else,条件数量较大时使用switch。在判断条件较多时,使用查找表比使用条件语句更快。 - 在
if-else语句中最有可能出现的条件放在首位。 - 当单个键和单个值之间存在逻辑映射关系时,就可以使用查找表,即将值放在数组或普通对象中,然后直接通过数组项或对象成员来访问数据,无需使用条件语句。
3,递归:
- 使用递归算法可能会导致栈溢出。可以用迭代(即在函数内使用循环)来代替递归。
五,快速响应的用户界面
1,浏览器的UI线程
- 用户执行
JavaScript和更新用户界面的进程通常被称为浏览器UI线程。它是一个单线程,它的工作基于一个简单的队列系统,任务会被保存在UI队列中直到进程(UI线程)空闲,一旦空闲,队列中的下一个任务就被提取出来到UI线程中并运行。这些任务要么是运行js代码,要么是执行UI更新。 - 因为当脚本执行时,
UI不随用户交互而更新。所以我们尽量保证单个js操作花费的总时间不超过100ms,否则会导致UI更新出现明显延迟。
2,使用定时器让出时间片段
- 定时器有助于把耗时较长的脚本拆分为较短的片段。
setTimeout(函数,时间):在多少毫秒后将任务添加到UI队列中。- 如果UI队列中已经存在由同一个
setInterval()创建的任务,那么后续任务不会被添加到UI队列中。 - 由于定时器精度问题,设置延迟的最小值建议为25ms,以确保至少有15ms的延迟(windows系统上定时器分辨率为15ms)。
3,Web Workers
web workers引入了一个接口,能使代码运行且不占用浏览器UI线程。web workers适用于那些处理纯数据,或者与浏览器UI无关的长时间(超过100毫秒)运行脚本:
编码/解码大字符串;
复杂数学运算(包括图像或视频处理);
大数组排序;
六,Ajax
1,数据传输
XMLHttpRequest,对于那些不会改变服务器状态,只会获取数据的请求,应该使用GET。经GET请求的数据会被缓存起来,如果需要多次请求同一数据的话,它有助于提升性能。
2,缓存数据
- 避免发送不必要的请求:
- 在服务端,设置
HTTP头信息以确保响应会被浏览器缓存。 - 在客户端,把获取到的信息存储到本地,从而避免再次请求。
七,编程实践
- 使用
Object/Array直接量来创建对象和数组。 - 尽量使用原生方法。比如需要进行复杂的数学运算时,尽量使用
Math对象。 - 使用
+加号连接字符串。
八,构建并部署高性能JavaScript应用
- 合并多个
js文件,减少页面渲染所需的http请求数。 js代码压缩。Accept-Encoding:gzip;
九,js性能分析
在脚本运行期间定时执行各种函数和操作,找出需要优化的部分。
使用console.time('a') console.timeEnd('a') 或 +new Date()来测试一段代码的执行时间。
1,chrome开发者工具
1,确定需要优化的地方:
无痕模式 -> performance面板 (设置cpu``6倍降速)-> record
- 若
FPS标红了,则说明此时帧率是有问题的,需要优化。帧率越高越流畅。 record时刷新,看network是否有异常。- 查看
calltree,它显示对应的文件或函数占用的时长,若有问题,则进行优化。 - 查看
main,有红色小三角形的部分可能存在问题。 - 节点(绿线)或
JS堆(蓝线)跳涨后不能回到原有水平,或绿线稳步增加从不减少,则说明存在内存泄露。(分析时可暂时先在代码中设置断点来暂停泄露)
2,内存泄露的处理:
若存在泄露,则通过memory面板,记录两个快照heap snapshot,进行比较,确定是哪部分存在泄漏。然后再根据record allocation stacks得出的快照来找出具体存在泄漏的地方(constructor -> allocation stack)。
Retained Size大的对象,可能造成的内存泄漏量也大。- 离目标对象距离一样的多个引用,其Distance值越小(即离GC Root越近)越可能是内存泄漏的根源。