📖 高性能 JavaScript

《高性能 JavaScript》这本书 2010 年出版, 书中的知识不见得都是有效或最佳的方案. 其中的一些思想在其他的编程语言中也多有应用.

加载和执行

  • </body> 闭合标签闭合之前, 将所有的 <script> 标签放到页面底部. 这能确保在脚本执行前页面已经完成渲染.
  • 合并脚本. 页面中的 <script> 标签越少, 加载也就越快, 响应也就更快. 无论外链文件还是内嵌脚本都是如此.
  • 有多种无阻塞下载 JavaScript 的方法:
    • 使用 <script> 标签的 defer 属性
    • 使用动态创建的 <script> 元素来下载并执行代码
    • 使用 XHR 对象下载 JavaScript 代码并注入页面

管理作用域

  • 访问直接量和局部变量的速度最快, 相反, 访问数组元素和对象成员相对较慢.
  • 由于局部变量存在于作用域的其实位置, 因此访问局部变量比访问跨作用于变量更快. 变量在作用域链中的位置越深, 访问所需时间就越长. 由于全局变量总是处在作用域链的最末端, 因此访问速度也是最慢的.
  • 避免使用 with 语句, 因为他会改变运行期上下文作用域链. 同样, try-catch 语句中的 catch 子句也有同样的影响, 因此也要小心使用.
  • 嵌套的对象成员会明显影响性能, 尽量少用. (window.location.href)
  • 属性或方法在原型链中的位置越深, 访问它的速度越慢.
  • 通常来说, 你可以通过把常用的对象成员、数组元素、跨域变量保存在局部变量中来改善 JavaScript 性能, 因为局部变量访问速度更快.

DOM 编程

  • 最小化 DOM 访问次数, 尽可能在 JavaScript 端处理.
  • 如果需要多次访问某个 DOM 节点, 请使用局部变量存储它的引用.
  • 小心处理 HTML 集合, 因为它实时联系着底层文档. 把集合的长度缓存到一个变量中, 并在迭代中使用它. 如果需要经常操作集合, 建议把它拷贝到一个数组中.
  • 如果可能的话, 使用速度更快的 API, 比如 querySelectorAll()firstElementChild.
  • 要留意重绘和重排; 批量修改样式时, “离线”操作 DOM 树, 使用缓存, 并减少访问布局信息的次数. (注:首先使节点脱离文档流, 比如使用 display:none , 然后修改节点, 最后再添加回文档, 从而减少文档重排或重绘次数)
  • 动画中使用绝对定位, 使用拖放代理.
  • 使用事件委托来减少事件处理器的数量.

算法和流程控制

  • for、while 和 do-while 循环性能特性相似, 所以没有一种循环类型明显快于或慢于其他类型.(for-in 循环除外, 它每次迭代会搜索实例或原型属性)
  • 避免使用 for-in 循环, 除非需要遍历一个属性未知的对象.
  • 改善循环性能的最佳方式是减少每次迭代的运算量和减少循环迭代次数.
  • 通常来说, switch 总是比 if-else 快, 但并不总是最佳解决方案.
  • 在判断条件较多时, 使用查找表比 if-else 和 switch 要快.
  • 浏览器的调用栈大小限制了递归算法在 JavaScript 中的应用; 栈溢出错误会导致其他代码中断运行.
  • 如果你遇到栈溢出错误, 可将方法改为迭代算法, 或使用 Memorization 来避免重复计算.

字符串和正则表达式

  • 当连接数量巨大或尺寸巨大的字符串时, 数组项连接是唯一在 IE7 及更早版本中性能合理的方法.
  • 如果不需要考虑 IE7 及更早版本的性能, 数组项连接是最慢的字符串连接方法之一.推荐使用简单的 + 和 += 操作符替代, 避免不必要的中间字符串.
  • 回溯失控发生在正则表达式本应该快速匹配的地方, 但因为某些特殊的字符串匹配动作导致运行缓慢甚至浏览器崩溃. 避免这个问题的办法是: 使相邻的字元互斥, 避免嵌套量词对同一字符串的相同部分多次匹配, 通过重复利用向前查看的原子组去除不必要的回溯.
  • 提高正则表达式效率的各种技术手段会有助于正则表达式更快地匹配, 并在非匹配位置上花更少的时间.
  • 正则表达式并不总是完成工作的最佳工具, 尤其当你只搜索字面字符串的时候.
  • 尽管许多方法可以去除字符串的首尾空白, 但使用两个简单的正则表达式(一个用来去除头部空白, 另一个用来去除尾部空白) 来处理大量字符串内容能提供一个简洁而跨浏览器的方法. 从字符串末尾开始循环向前搜索第一个非空白字符, 或者将此技术同正则表达式结合起来, 会提供一个更好的替代方案, 它很少受到字符串长度的影响.

快速响应的用户界面

  • 任何 JavaScript 任务都不应该执行超过 100 毫秒. 越长的运行时间会导致 UI 更新出现明显的延迟, 从而对用户体验产生负面影响.
  • JavaScript 运行期间, 浏览器响应用户交互的行为存在差异. 五路如何, JavaScript 长时间运行将导致用户体验变得混乱和脱节.
  • 定时器可用来安排代码延迟执行, 它使得你可以把长时间运行脚本分解成一系列的小任务.
  • Web workers 是新版浏览器支持的特性, 它允许你在 UI 线程外部执行 JavaScript 代码, 从而避免锁定 UI.

Ajax

  • 减少请求数, 可通过合并 JavaScript 和 CSS 文件, 或使用 MXHR.
  • 缩短页面的加载时间, 页面主要内容加载完成后, 用 Ajax 获取那些次要的文件.
  • 确保你的代码错误不会输出给用户, 并在服务端处理错误.
  • 知道何时使用成熟的 Ajax 类库, 以及何时编写自己的底层 Ajax 代码.

编程实践

  • 通过避免使用 eval()Function() 构造器来避免双重求值带来的性能小号. 同样的, 给 setTimeout()setInterval() 传递函数而不是字符串作为参数.
  • 尽量使用直接量创建对象和数组. 直接量的创建和初始化都比非直接量形式要快.
  • 避免做重复的工作. 当需要检测浏览器时, 可使用延迟加载或条件预加载.
  • 在进行数学计算时, 考虑使用直接操作数字的二进制形式的位运算.
  • JavaScript 的原生方法总会比你写的任何代码都要快. 尽量使用原生方法.

构建并部署高性能 JavaScript 应用

  • 合并 JavaScript 文件减少 HTTP 请求
  • 使用 YUI Compressor 压缩 JavaScript 文件.
  • 在服务器端压缩 JavaScript 文件 (Gzip 编码).
  • 通过正确设置 HTTP 响应头来缓存 JavaScript 文件, 通过向文件名增加时间戳来避免缓存问题.
  • 使用 CDN 提供 JavaScript 文件; CDN 不仅可以提升性能, 它也为你管理文件的压缩与缓存.

工具

  • 使用网络分析工具找出加载脚本和页面中其他资源的瓶颈, 这会帮助你决定哪些脚本需要延迟加载, 或需要进一步分析.
  • 尽管传统的经验告诉我们要尽量减少 HTTP 请求数, 但把脚本尽可能延迟加载可以加快页面的渲染速度, 给用户带来更好的体验.
  • 使用性能分析工具找出脚本运行过程中速度慢的地方, 检查每个函数所消耗的时间, 以及函数被调用的次数, 通过调用栈自身提供的一些线索来找出需要集中精力优化的地方.
  • 尽管耗费的时间和调用的次数通常是数据中最有价值的部分, 但仔细观察函数的调用过程, 你也许会发现其他优化目标.