性能优化

223 阅读8分钟

一,脚本的加载和执行

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,缓存数据

  1. 避免发送不必要的请求:
  • 在服务端,设置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越近)越可能是内存泄漏的根源。