Skip to content

JavaScript 性能

编写高效的 JavaScript 代码对于创建快速响应的 Web 应用程序至关重要。本节介绍优化 JavaScript 性能的关键策略。

循环很常见,循环内部的任何低效率都会因迭代次数而放大。通过将计算或属性访问移出循环来优化,如果它们不随每次迭代而改变。

低效示例(重复访问 arr.length):

// Bad: arr.length is accessed in each iteration
// 不好:每次迭代都访问 arr.length
// const arr = new Array(1000).fill(0);
// for (let i = 0; i < arr.length; i++) {
// // Do something
// // 执行一些操作
// }

更高效(缓存 arr.length):

const arr = new Array(1000).fill(0);
const len = arr.length; // Cache the length
// 缓存长度
for (let i = 0; i < len; i++) {
// Do something
// 执行一些操作
}
// Modern JavaScript engines are often smart enough to optimize this automatically,
// 现代 JavaScript 引擎通常足够智能,可以自动优化这一点,
// but for very large arrays or performance-critical code, caching can still be beneficial.
// 但对于非常大的数组或性能关键的代码,缓存仍然可能是有益的。

访问和操作文档对象模型 (DOM) 比其他 JavaScript 操作慢得多。最小化 DOM 交互:

  • 缓存 DOM 元素: 如果需要多次访问同一个 DOM 元素,只需查询一次并将其存储在变量中。
  • 批量更新 DOM: 尝试通过更少的操作来进行更改,而不是对 DOM 进行许多小更改。例如,在内存中构建 HTML 字符串或 DocumentFragment,然后一次性添加到 DOM 中。
  • 避免布局抖动 (Layout Thrashing): 在循环中重复读取布局属性(如 offsetHeight 或 getComputedStyle)然后写入 DOM 会强制浏览器多次重新计算布局,这非常耗费性能。如果可能,请分组读取和写入操作。

缓存 DOM 元素的示例:

// Inefficient:
// 低效:
// document.getElementById("myElement").innerText = "First update";
// document.getElementById("myElement").style.color = "blue";
// Efficient:
// 高效:
// const myElement = document.getElementById("myElement");
// if (myElement) {
// myElement.innerText = "First update";
// myElement.style.color = "blue";
// }

使用 DocumentFragment 进行批量更新:

// const list = document.getElementById("myList");
// const items = ["Apple", "Banana", "Cherry"];
// const fragment = document.createDocumentFragment();
// items.forEach(itemText => {
// const li = document.createElement("li");
// li.textContent = itemText;
// fragment.appendChild(li);
// });
// list.appendChild(fragment); // Single append operation to the live DOM
// list.appendChild(fragment); // 单次添加到实际 DOM 中的操作

大型 DOM 树(许多 HTML 元素)会降低页面渲染、CSS 样式应用以及 DOM 操作的速度。力求精简的 DOM 结构。

这不仅改善了初始页面加载时间,还使得后续的 DOM 查询和操作更快。

以前的建议是“如果你不打算保存值,就不要创建新变量。”例如:

// Old style:
// 旧风格:
// const firstName = "John";
// const lastName = "Doe";
// const fullName = firstName + " " + lastName;
// document.getElementById("demo").innerHTML = fullName;

可以写成:

// const firstName = "John";
// const lastName = "Doe";
// document.getElementById("demo").innerHTML = firstName + " " + lastName;

然而,现代 JavaScript 引擎非常擅长优化变量分配。优先考虑代码的可读性和可维护性。命名良好的中间变量可以使代码更容易理解。仅在性能分析表明某个关键路径存在瓶颈时,才进行微观优化变量使用。

JavaScript 的加载方式和时机对页面加载性能和感知速度有显著影响。

  • 将脚本放在 <body> 底部: 这允许浏览器先解析和渲染 HTML 内容,然后才遇到阻塞页面解析的 <script> 标签。
  • 使用 async 属性: <script async src="script.js"></script> 异步下载脚本(不阻塞 HTML 解析),并在下载完成后立即执行。如果存在多个带有 async 的脚本,执行顺序不保证。适用于分析脚本等独立脚本。
  • 使用 defer 属性: <script defer src="script.js"></script> 异步下载脚本,但在 HTML 文档完全解析后,在 DOMContentLoaded 事件之前,按顺序执行。这通常是非关键脚本的最佳选择,它们需要 DOM 准备就绪并应按特定顺序执行。
  • 动态加载脚本: 当需要时以编程方式加载脚本,例如在用户交互后或当页面某个部分变得可见时。

动态加载示例:

// function loadMyScript() {
// const script = document.createElement("script");
// script.src = "myHeavyScript.js";
// document.body.appendChild(script);
// }
// // Call loadMyScript() when appropriate, e.g., on a button click.
// // 在适当的时候调用 loadMyScript(),例如在按钮点击时。

with 关键字会扩展作用域链,这使得 JavaScript 引擎难以优化代码,并可能导致变量解析的歧义。它还会对性能产生负面影响。

with 在严格模式下被禁止,并且通常被认为是坏习惯。完全避免使用它。

  • 使用高效的算法和数据结构: 为任务选择正确的工具。例如,对于某些操作(如频繁添加/删除或检查是否存在),Map 或 Set 可能比普通对象或数组性能更好。
  • 防抖 (Debouncing) 和 节流 (Throttling): 对于频繁触发的事件处理程序(例如 scroll、resize、input),使用防抖(在一定时间没有活动后才执行函数)或节流(在指定时间间隔内最多执行一次函数)来限制调用次数。
  • 记忆化 (Memoization): 缓存耗时函数调用的结果,并在输入相同时返回缓存的结果。
  • Web Workers: 对于不需要访问 DOM 的 CPU 密集型任务,将其卸载到 Web Workers 中在单独的线程中运行,以保持主 UI 线程响应。
  • 代码分割 (Code Splitting) 和 Tree Shaking: 对于大型应用程序,使用支持代码分割(将代码分割成按需加载的小块)和 Tree Shaking(从最终打包中移除未使用的代码)的打包工具(如 Webpack、Rollup、Parcel)。
  • 对代码进行性能分析 (Profiling): 使用浏览器开发者工具(Performance 选项卡)识别 JavaScript 中的瓶颈。不要过早优化;专注于实际慢的区域。