《高性能javascript》笔记

2018-03-15

第一章 加载和执行

知识点

由于页面在加载遇到script标签时,会下载并执行js文件,从而阻塞包括页面绘制在内的其他浏览器进程,因此需要优化。

提高性能措施

  • 将所有script标签放在页面底部</body>闭合标签之前,可以确保脚本执行前页面已渲染完成;
  • 合并脚本,减少http请求数,提升加载速度;
  • 使用无阻塞下载js文件的方法,包括:1)使用带defer属性的script标签;2)动态创建script元素来下载并执行代码;3)使用XHR对象下载js代码并注入页面。

第二章 数据存取

知识点

  • 数据存储共有4章方式:字面量、变量、数组项、对象成员,字面量和局部变量访问最快,数组元素和对象成员访问较慢;
  • 变量在作用域链中位置越深,访问越慢,因此使用闭包时需要注意性能(闭包由于要保存外部环境的活动对象,还会占用内存);
  • with语句、try-catch的catch字句会改变执行环境作用域链,小心使用;
  • with语句、try-catch的catch字句、eval()函数会造成动态作用域,不利于javascript引擎对在作用域中访问变量速度的优化;
  • 对象成员的访问由于涉及原型链,访问成员时和作用域链中访问变量类似;
  • 嵌套的对象会影响访问速度。

提高性能措施

如果一个(嵌套)对象成员、数组元素、跨域变量,在值不变的情况下被多次重复使用,可以将其保存为局部变量。

第三章 DOM编程

DOM作为独立于es语言的api,对其的访问与修改就像是在两座岛屿间穿行,因此尽量在es内部完成大部分处理,只用少量的操作访问和修改DOM。

DOM的访问与修改

  • 除了基于Webit的新版浏览器外,innerHTML相对DOM方法进行DOM修改操作速度更快;
  • 节点克隆和创建元素无明显性能差异;
  • HTML集合是“假定实时态”的,尤其需要注意性能和避免逻辑bug,技巧包括:缓存集合长度length,多次访问统一集合内节点时先保存为局部变量;
  • 在围绕某个元素展开遍历时, nextSiblingchildNodes两种方式不相上下,老式IE中nextSibling遍历更快;
  • 直接获取元素节点的新API(如nodeRef.children)和功能强大的选择器API(如nodeRef.querySelectorAll())速度大幅提升。

重绘与重排

浏览器在加载完页面资源后除生成DOM树外还会生成渲染树,利用渲染树绘制页面;页面几何属性有变化时会重排,然和结合其他最新非几何属性进行重绘。

  • 对布局信息的获取(如获得计算属性)会刷新页面渲染树计算任务队列并触发重排;
  • 为了最小化重排和重绘,技巧包括:修改样式时利用el.style.cssText一次性批量修改或修改样式类、批量DOM修改在脱离文档流的情况下进行(隐藏-修改-显示、拷贝-修改-替换、使用文档片段);
  • 缓存布局信息从而避免多次重复请求布局信息;
  • 让动画元素脱离页面流(如绝对定位),动画完成后再让其返回页面流,从将重排的影响限制在动画元素本身,避免整个页面大量元素的高频度重排;
  • IE的:hover的使用范围扩展,但应避免页面大量使用这种效果而引起性能问题。

事件委托

过多的事件绑定会占用浏览器处理时间和事件追踪的内存消耗,因此可以通过事件委托减少总体事件处理程序数量,提升性能。

第四章 算法和流程控制

循环

  • 有四种循环方式:for、while、do while、for in;
  • for in性能明显比其余三种差,如果不是需要遍历一个未知属性数量的对象,尽量不要使用;
  • 另外三种循环方式性能接近,根据需求使用;
  • 改善循环性能1:减少每次迭代运算量(将数组长度保存为变量从而减少属性查找、数组倒序迭代);
  • 改善循环性能2:减少循环迭代次数(达夫装置可以提升次数巨大的迭代的性能);
  • 原生的基于函数的迭代,如es4开始引入Array的forEach方法,由于存在外部方法调用,性能反而不如直接的循环方式如for循环。

条件判断

  • 从性能和逻辑清晰度角度看,多分支使用swiitch,少分支使用if-else;
  • 减少if-else条件判断次数的方法:将高频成功的分支放在前面、使用二分法(如果是大量离散值,使用switch更好);
  • 如果是单键和单值间的映射,则采用查找表(如数组)。

递归

  • 递归常见问题:无明确终止条件、递归次数超出浏览器调用栈大小限制;
  • 两种递归模式:函数直接调用自身、函数间相互调用(这种出现错误时很难定位);
  • 解决调用栈大小限制方法:改用迭代、使用Memoization技术(缓存之前调用结果)。

第五章 字符串和正则表达式

字符串连接

  • 连接大数量/大尺寸字符串时,数组项合并(join方法)是唯一在IE7及更高版本浏览器中保持性能合理的方法;
  • 在主流浏览器中,+和+=操作符性能明显更好,数组项合并和字符串的concat方法性能都较差。

正则表达式优化

  • 回溯是其基本工作原理,也是性能问题主要源头;
  • 避免回溯失控方法:让相邻字元互斥、避免嵌套量词对同一字符串的相同部分多次匹配、重复利用预查原子组去除不必要回溯;
  • 更多优化方法:1)提高匹配失败速度;2)以简单、必要的字元开头;3)使用量词模式,使其与后面的字元互斥;4)减小分支数量、缩小分支范围;5)使用捕获组;6)让捕获组只捕获需要的文本,较少后处理;7)暴露必须的字元;8)使用合适的量词;9)把正则表达式赋给变量,避免重复编译;10)将复杂正则表达式拆分为多个简单的正则表达式;
  • 充分利用字符串自有的一些方法,其速度都较快。

去除字符串首尾空白

  • 使用两个简单正则表达式分别去除首尾空白具有很好的性能和兼容性。

第六章 快速响应的用户界面

浏览器UI线程

  • javascript和用户界面更新在同一个进程中进行,因此一次只能处理一个任务。因此当javascript代码在运行时会导致用户界面不能响应输入,反之亦然。高效管理UI线程要求js运行时间不能太长,以免影响用户体验;
  • 浏览器对js的运行通常有两种限制:调用栈大小限制、长时间运行脚本限制;
  • 长时间在不同浏览器有不同的定义,从用户体验来说,100ms是一个重要时间限制,对程序员来说可以将js任务限制在50ms内。

使用定时器让出时间片段

  • 创建定时器会造成UI线程暂停,如同任务切换,因此会重置相关浏览器限制(如长时间运行脚本);
  • 定时器的时间是将任务加入队列的时间;
  • windows系统中定时器精度是15ms,因此定时器延时最小值建议25ms;
  • 使用定时器可以对运行时间长的任务(无同步要求、无数据处理顺序强求)进行分解;
  • 通过记录代码运行时间,可以精细控制代码任务拆分,提高性能;
  • 尽量限制高频率重复定时器的数量,以免带来性能问题。

Web Workers

  • html5新API,能够独立于UI线程开辟自己的线程,但是可访问资源有限制;
  • 消息系统是网页和worker通信的唯一接口;
  • worker可以用importScripts()方法加载外部文件;
  • Web Worker适用于处理纯数据或与浏览器UI无关的长时间运行脚本,超过100ms的任务使用它可能比使用定时器拆解更合适。

第七章 Ajax

数据传输

请求数据

下面五种方式,前三者最常用,后两者往往用在极端情况下。

XHR

  • 完善的控制和灵活性
  • 数据作为字符串,解析速度可能降低

动态脚本注入

  • 可以实现跨域
  • 有潜力成为客户端获取并解析数据的最快的方法
  • 不能读取头部信息或响应代码,不够安全

Multipart XHR

  • 通过明显减少请求数量改善性能
  • 不能缓存请求的数据、低版本浏览器可能不支持

iframes

Comet

发送数据

XHR

  • 通用、灵活,POST可以发送大量数据

Beacons

  • 发送少量数据非常方便(get)
  • 能力有限(不能POST、URL长度限制、接收数据信息少)

数据格式

JSON和自定义格式轻量从而性能更好;JSONP可跨域,解析极快,但要避免涉及敏感数据。

XML

  • 广泛而支持良好
  • 笨重,解析缓慢,性能很差,如果有其他格式可用,尽量不使用

JSON

  • 轻量,数据占响应更大比例,易解析,通用性高,尤其在动态脚本注入时性能很好

    HTML

  • 缓慢、臃肿,在客户端CPU瓶颈超过带宽(比如极大量的字符串操作使得解析很慢)时才使用

自定义格式

  • 十分轻量,对于大数据集具有明显的优势
  • 但需要服务端构造程序并在客户端解析

其他方面性能措施

缓存数据

  • 设置服务器端响应头部
  • 客户端手工缓存

了解Ajax类库的局限

  • 为了统一的兼容性,通常功能不够完整,必要时可以直接使用XHR对象

总结

  • 减少请求数,合并文件或使用MXHR
  • 缩短页面加载事件,主要内容加载完成后再用ajax获取次要文件
  • 确保代码错误不会输出给用户,并在服务端处理错误
  • 根据场景考虑使用类库还是自己编写底层代码

第八章 编程实践

  • 在eval()、Function()、setTimeout()、setInterval()中传入字符串形式的代码作为参数会引起双重求值而带来性能消耗,尽量避免,后两者可传入函数作为参数。
  • 经历使用字面量创建对象和数组。
  • 避免重复的工作。需要浏览器检测的时候,可使用延迟加载或条件预加载。
  • 数学计算时,考虑使用直接操作数字的二进制形式的位运算;复杂数学运算优先考虑Math对象。
  • Javascript的元素方法总比我们自己编写的代码快,尽量使用原生方法。

第九章 构建并部署高性能JavaScript应用

  • 合并JavaScript文件以减少请求数
  • 压缩JavaScript文件
  • 在服务器端压缩JavaScript文件
  • 正确设置HTTP相应头来缓存JavaScript文件,通过向文件名增加时间戳来避免缓存问题
  • 使用CDN来提供JavaScript文件,它不仅提升性能,还能为你管理文件的压缩与缓存
  • 使用构建工具,能使这些过程自动高效地完成

(本文完)

知识共享许可协议
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。