《高性能 JavaScript》这本书是前端性能优化领域的奠基之作,即使今天看来,其中的大部分原则和思想依然极具指导意义。全书的核心思想是:JavaScript 的执行、DOM 的交互以及网络的传输是 Web 应用中最主要的性能瓶颈,优化应围绕这三者展开。


第1部分:加载与执行

这部分关注的是 JavaScript 代码如何被浏览器下载、解析和执行,以及其对页面渲染的影响。

1. 脚本阻塞特性

1)核心问题:浏览器在遇到 <script> 标签(无论是内联还是外联)时,会暂停 HTML 解析、暂停 DOM 构建,先去下载(对于外联)和执行脚本。

2)后果:将脚本放在 <head> 中或页面顶部,会导致明显的页面白屏,用户需要等待所有脚本加载执行完毕后才能看到页面内容。

2. 优化策略

1)将脚本置于底部:将所有的 <script> 标签放在 <body> 闭合标签之前。这能确保页面内容先被渲染出来。

2)合并脚本:减少 HTTP 请求次数。将多个小文件合并成一个大文件,因为每个 HTTP 请求都会带来额外的性能开销。

3)使用无阻塞模式

① defer 属性:脚本会被延迟到整个页面都解析完毕后再运行,且会按照声明的顺序执行。(推荐)

② async 属性:脚本一旦下载完毕就会立即执行,不保证执行顺序,适用于无依赖的独立脚本(如统计分析)。

③ 动态脚本注入:使用 DOM API 动态创建 <script> 元素并添加到页面中。这种方式下载和执行脚本不会阻塞页面其他进程。

4)使用 XHR 脚本注入:通过 AJAX 请求获取脚本内容,然后通过 eval() 或创建 <script> 标签并设置其 text 属性来执行。这种方式可以精确控制脚本的执行时机。


第2部分:数据访问

这部分探讨了 JavaScript 中数据的存储位置对其访问速度的影响。

1. 作用域链与标识符解析

1)JavaScript 中数据的存储位置有四种,访问速度从快到慢依次为:

① 字面量 / 常量

② 局部变量(在函数内部用 varletconst 声明的)

③ 外部作用域变量(闭包中的外层变量)

④ 全局变量

2)根本原因:引擎在查找变量时,会沿着作用域链 逐级向上搜索。作用域链的层级越深,查找耗时就越长。全局变量总是处于作用域链的最末端,所以最慢。

2. 原型链与对象成员

1)访问对象的属性(如 obj.name)也会触发一个搜索过程,即沿着原型链查找。

2)嵌套越深的属性,访问越慢(如 obj.style.color)。

3. 优化策略

1)多用局部变量:如果某个全局变量或外部作用域变量被多次使用,应将其存入局部变量中。

// 慢
function foo() {
    for (var i = 0; i < document.getElementsByTagName('div').length; i++) {
        // ...
    }
}

// 快
function foo() {
    var divs = document.getElementsByTagName('div'); // 存入局部变量
    for (var i = 0; i < divs.length; i++) {
        // ...
    }
}

2)避免使用 with:它会动态改变作用域链,导致引擎无法进行优化。

3)缓存对象成员:对于需要多次访问的嵌套对象成员,可以将其缓存到局部变量中。


第3部分:DOM 编程

DOM 操作是 JavaScript 中最耗性能的部分之一,因为 DOM 和 JavaScript 是由不同引擎实现的,它们通过接口连接,每次交互都有性能损耗。

1. DOM 访问与操作的成本

1)“过桥费”昂贵:每次访问或修改 DOM,都会在 JavaScript 引擎和渲染引擎之间产生通信开销。

2)重排与重绘

① 重排:当 DOM 的变化影响了元素的几何属性(如宽、高、位置),浏览器需要重新计算元素的几何属性,并重新构建渲染树,这个过程称为“重排”或“回流”。

② 重绘:当元素的变化不影响布局,只影响外观(如颜色、背景色),则会发生“重绘”。

③ 核心结论重排的成本远高于重绘,因为它会导致后续所有子元素和兄弟元素的重排与重绘。

2. 优化策略

1)最小化 DOM 访问:在 JavaScript 端处理数据,准备好所有更改后,再一次性操作 DOM。

2)使用局部变量缓存 DOM 引用:如果同一个 DOM 属性或方法被多次访问,一定要用局部变量缓存它。

3)批量修改 DOM

① 使元素脱离文档流:先进行隐藏(display: none)或使用 DocumentFragment,然后在上面进行多次修改,最后再将其放回文档流。

② 克隆节点:克隆一个已有的节点,在副本上修改,然后用副本替换原节点。

4)避免触发同步布局(或称为“强制同步重排”):

// 糟糕的写法:每次循环都触发重排
for (var i = 0; i < divs.length; i++) {
    divs[i].style.height = divs[i].offsetWidth + 'px';
}

// 好的写法:先读取,后写入
var width = div.offsetWidth; // 先读取一次
for (var i = 0; i < divs.length; i++) {
    divs[i].style.height = width + 'px'; // 然后统一写入
}

1)在循环中连续读取会导致重排的样式属性(如 offsetTopscrollLeft 等),会强制浏览器为了提供最新值而立即进行重排,严重降低性能。

2)使用事件委托:利用事件冒泡,将事件处理程序绑定到父元素上,而不是每个子元素上。这减少了内存占用和绑定的时间。


第4部分:算法和流程控制

这部分探讨了代码本身的执行效率。

1. 循环优化

1)倒序循环:通常最快,因为减少了每次循环的条件判断(与 0 比较)。

2)减少每次循环的工作量:比如缓存数组长度 for (var i=0, len=items.length; i < len; i++)

3)达夫设备:一种循环展开技术,用于在极端情况下优化大量数据的迭代。

2. 条件语句优化

1)if-else vs switch:当条件数量较少时(例如少于4个),if-else 更易读;当条件数量较多且是离散值时,switch 通常更快。

2)查表法:将条件与结果的映射关系放在一个数组或对象中,通过索引直接获取结果。这在有大量条件分支时性能极佳。

// 查表法示例
var results = [result0, result1, result2, result3, result4];
return results[value];

3. 递归优化

1)递归可能导致栈溢出错误。

2)使用迭代代替递归

3)使用 Memoization(记忆化):缓存已经计算过的结果,避免重复计算。


第5部分:字符串和正则表达式

1. 字符串连接优化

1)在旧版浏览器中,使用 + 或 += 连接大量字符串性能极差,因为它会重复创建新的字符串对象。

2)数组项连接是当时性能最好的方法:array.join(””)

3)现代引擎优化:在现代 JavaScript 引擎(如 V8)中,使用 + 或 += 已经被高度优化,通常性能很好,但 array.join(”) 依然是一个可靠的选择。

2. 正则表达式优化

1)关注起始位置:正则引擎会优化以非通配符开头的表达式。

2)减少回溯:贪婪量词和嵌套量词是导致灾难性回溯的元凶。

3)具体策略

① 使用具体、简单的量词(如 [^”] 代替 .*?)。

② 使用非捕获组 (?: ... ) 来节省捕获开销。

③ 将复杂的正则表达式拆分为多个简单的。


第6部分:快速响应的用户界面

浏览器的 JavaScript 运行环境是单线程的,这意味着长时间的 JavaScript 执行会阻塞 UI 更新和用户交互。

1. 浏览器 UI 线程

1)用于执行 JavaScript 和更新 UI 的是同一个线程。

2)任务被放入一个队列中,按顺序执行。

2. 优化策略

1)定时器分解任务

① 如果一段代码运行时间超过 50 毫秒,就可能被用户感知到延迟。

② 使用 setTimeout 或 setInterval 将一个大任务分解为多个小任务,在任务间隙让出 UI 线程的控制权,使得浏览器可以响应用户输入和更新 UI。

2)Web Workers

① 用于在后台线程中运行脚本,处理密集型计算任务而不阻塞 UI 线程。

② 限制:Worker 中不能访问 DOM、window 或 document 对象。与主线程通过消息机制 postMessage 通信。


第7部分:Ajax

1. 数据传输

1)请求数据:尽可能使用 GET 请求,因为它可以被缓存。

2)数据格式

① JSON:体积小、解析速度快(使用 JSON.parse()),是首选格式。

② JSON-P:可以跨域,但安全性需要注意。

③ HTML:省去了在客户端创建 DOM 节点的步骤,但通常比数据格式更臃肿。

④ XML:冗长且解析慢,应避免使用。

2. 优化策略

① 缓存数据:在服务端设置正确的 HTTP 缓存头,或在客户端将已获取的数据存储起来。

② 使用 CDN


第8部分:编程实践

1. 避免双重求值

1)使用 eval()Function() 构造函数, setTimeout() 和 setInterval() 传递字符串参数时,会导致在运行时启动一个新的解释器来执行字符串代码,性能代价极高。

2)始终避免使用 eval()

2. 使用对象和数组的直接量

使用 {} 和 [] 创建对象和数组,比使用 new Object() 和 new Array() 更快且更简洁。

3. 位操作

在执行数学运算时,位运算符(如 ~~>><<|)比纯 JavaScript 方法更快,因为它们直接在底层硬件上操作。


第9 & 10 部分:构建与部署、工具

1. 构建与部署

1)合并文件:减少 HTTP 请求数。

2)压缩代码:使用工具(如 UglifyJS, Terser)移除所有注释、空白符,并缩短变量名。

3)启用 Gzip 压缩:在服务器端对文本资源进行 Gzip 压缩。

4)设置 HTTP 缓存头:让浏览器缓存静态资源。

5)使用 CDN

2. 性能分析工具

1)Profiler:浏览器自带的性能分析工具(如 Chrome DevTools 的 Performance 和 Memory 面板),可以找到代码的瓶颈。

2)识别内存泄漏:常见原因是意外的全局变量、分离的 DOM 节点引用和闭包。

💡总结

《高性能 JavaScript》的核心可以概括为:

1)加载时无阻塞,少请求。

2)数据访问:多用局部变量,少找全局和原型。

3)DOM 操作批量处理,避免重排,善用事件委托

4)代码本身:优化循环条件分支,避免长时间运行的任务阻塞 UI。

5)整体架构:善用 Ajax 和 Web Workers 实现异步和解耦。

6)工程化合并、压缩、缓存是发布前的必备步骤。

虽然这本书写于多年前,部分具体技术(如模块化、构建工具)已被 Webpack、Vite 等现代工具自动化解决,但其背后的核心思想和优化原则至今依然是每一位前端开发者必须理解和掌握的内功心法。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐