Skip to content

JavaScript 性能优化

性能优化是一个持续的过程,旨在提升用户体验、减少资源消耗、提高应用响应速度。在 JavaScript 中,优化不仅仅意味着写更快的代码,还包括如何高效地与浏览器、网络和用户交互。

1. 性能优化基本原则 (The Golden Rules)

无论何种场景,性能优化都遵循以下几个核心原则:

  1. 减少 (Reduce): 减少代码量、减少资源大小、减少 HTTP 请求、减少 DOM 操作。
  2. 延迟 (Defer): 延迟加载不必要的资源,延迟执行非关键代码。
  3. 优化 (Optimize): 优化算法、优化数据结构、优化渲染流程、利用缓存。
  4. 测量 (Measure): 没有测量就没有优化。在优化前后进行性能测试,确保改进有效。
  5. 不作不必要优化: 过早优化是万恶之源。只优化瓶颈,而非所有代码。

2. 代码执行优化 (Code Execution Optimization)

JavaScript 代码的执行速度直接影响应用响应。

2.1 优化循环 (Loop Optimization)

  • 减少循环内部工作: 将可以提前计算或缓存的值放到循环外部。
    js
    // Bad
    for (let i = 0; i < arr.length; i++) {
      console.log(arr[i], arr.length); // arr.length 每次都计算
    }
    
    // Good
    const len = arr.length;
    for (let i = 0; i < len; i++) {
      console.log(arr[i], len); // len 只计算一次
    }
  • 选择合适的循环结构: for...of 通常比 forEach 性能更好(特别是在大数据量时),因为它没有函数调用开销。for 循环(for (let i = 0; i < len; i++))通常是最快的。
  • 避免在循环中创建函数: 闭包创建和垃圾回收会产生开销。

2.2 优化 DOM 操作 (DOM Manipulation Optimization)

DOM 操作是 JavaScript 中最昂贵的操作之一。

  • 减少 DOM 访问次数: 缓存 DOM 元素的引用。
    js
    // Bad
    document.getElementById('my-element').style.color = 'red';
    document.getElementById('my-element').textContent = 'Hello';
    
    // Good
    const myElement = document.getElementById('my-element');
    myElement.style.color = 'red';
    myElement.textContent = 'Hello';
  • 批量操作 DOM (DocumentFragment): 当需要添加大量 DOM 元素时,先在 DocumentFragment 中构建,然后一次性添加到 DOM 树。
    js
    const fragment = document.createDocumentFragment();
    for (let i = 0; i < 1000; i++) {
      const li = document.createElement('li');
      li.textContent = `Item ${i}`;
      fragment.appendChild(li);
    }
    document.getElementById('my-list').appendChild(fragment); // 只触发一次重排
  • 避免不必要的布局计算 (Reflow/Layout): 频繁读取像 offsetHeight, offsetWidth, getComputedStyle() 等属性会导致浏览器强制重新计算布局。
  • 使用 CSS 类名进行样式修改: 批量修改样式比单独修改 style 属性更高效。

2.3 优化算法和数据结构 (Algorithm & Data Structure Optimization)

  • 选择合适的算法: O(n²) 的算法在数据量大时,性能会远低于 O(n log n) 或 O(n)。
  • 选择合适的数据结构:
    • Map vs Object: 当键不是字符串或需要保持插入顺序时,Map 性能更好。
    • Set vs Array: Set 在查找(has)和去重方面比 Array 快得多(O(1) vs O(n))。
  • 缓存计算结果 (Memoization): 对于计算成本高且输入参数不变的函数,缓存其结果。

2.4 避免全局变量和隐式全局变量

  • 过多的全局变量会增加命名冲突的风险,更重要的是,它们会延长变量的生命周期,可能导致内存泄漏。
  • 使用 var 声明但未在函数内部声明的变量会自动成为全局变量。始终使用 constlet

3. 网络性能优化 (Network Performance Optimization)

这部分优化通常与前端构建和后端配置有关,但 JavaScript 层面也有贡献。

3.1 减少 HTTP 请求 (Reduce HTTP Requests)

  • 合并文件: 将多个 JS 文件合并成一个(通过打包工具,如 Webpack)。
  • CSS Sprites: 将多张小图片合并成一张大图(CSS 优化,减少图片请求)。
  • Base64 编码: 将小图片编码为 Base64 字符串直接嵌入 CSS 或 JS,减少请求。

3.2 减少传输大小 (Reduce Transfer Size)

  • 代码压缩 (Minification): 移除空格、注释、缩短变量名(通过 UglifyJS, Terser)。
  • Tree Shaking: 移除未使用的模块代码(通过 Webpack, Rollup)。
  • Gzip/Brotli 压缩: 服务器端启用 HTTP 压缩,减小文件传输大小。
  • 图片优化: 压缩图片、使用 WebP 等新格式。

3.3 缓存 (Caching)

  • HTTP 缓存: 设置 Cache-Control, Expires, ETag, Last-Modified 等 HTTP 响应头。
  • Service Workers: 实现离线缓存、网络请求代理,提供更细粒度的控制。
  • LocalStorage/SessionStorage: 缓存静态数据。

3.4 延迟加载 (Lazy Loading) 与预加载 (Preloading)

  • 延迟加载 (Lazy Loading): 按需加载。
    • 图片: 仅当图片进入视口时才加载。
    • 组件/模块: 路由懒加载、组件懒加载(Webpack import())。
  • 预加载 (Preloading/Prefetching): 在浏览器空闲时,提前加载用户可能需要的资源。
    • <link rel="preload">, <link rel="prefetch">
    • Webpack 的 /* webpackPreload: true */

3.5 使用 CDN (Content Delivery Network)

  • 将静态资源部署到 CDN,利用 CDN 的全球节点优势,加速内容分发。

4. 渲染性能优化 (Rendering Performance Optimization)

这主要发生在浏览器中,涉及 JS 如何影响页面的布局和绘制。

4.1 避免强制同步布局 (Avoid Forced Synchronous Layouts)

  • 浏览器通常会收集所有 DOM/CSS 更改,然后一次性计算布局和绘制(即异步)。
  • 强制同步布局: 当你修改了 DOM 样式,然后立即读取某个布局属性(如 offsetHeight, getBoundingClientRect()),浏览器会立即执行布局计算,以确保返回的值是最新的。这会导致回流 (Reflow),如果频繁发生,会严重影响性能。
    js
    const el = document.getElementById('my-div');
    el.style.width = (el.offsetWidth + 10) + 'px'; // 读取 -> 写入 -> 读取,导致每次都回流
    解决方案: 批量读写,避免读写交错。

4.2 减少回流 (Reflow/Layout) 和重绘 (Repaint)

  • 回流: 页面布局发生变化时(如元素大小、位置改变),浏览器需要重新计算元素的位置和大小。回流通常会导致重绘。
  • 重绘: 元素样式改变,但布局不变时(如颜色、背景色改变),浏览器需要重新绘制元素。
  • 策略:
    • 脱离文档流: 复杂动画使用 position: absolutefixed,减少对其他元素的影响。
    • 使用 CSS Transform 和 Opacity: 这些属性通常由 GPU 加速,不会触发布局或绘制。
    • 使用 will-change 属性: 提前告知浏览器哪些属性将发生变化,浏览器可以进行优化。
    • 合理使用 display: none: 隐藏元素会触发一次回流,但之后的操作不会影响布局,直到再次显示。

4.3 动画优化 (Animation Optimization)

  • 优先使用 CSS 动画/过渡: 浏览器可以对其进行优化,并将其放在独立的线程上执行,不阻塞 JS 主线程。
  • 使用 requestAnimationFrame() (rAF): 实现 JS 驱动的动画。rAF 会在浏览器下一次重绘之前调用回调函数,确保动画与浏览器刷新率同步,避免丢帧。
    js
    function animate() {
      // 修改 DOM
      requestAnimationFrame(animate); // 递归调用
    }
    requestAnimationFrame(animate);

5. 内存管理优化 (Memory Management Optimization)

防止内存泄漏,高效使用内存。

5.1 避免内存泄漏 (Avoid Memory Leaks)

  • 全局变量: 避免不必要的全局变量,它们永不被回收。
  • 未清除的定时器: setInterval, setTimeout 在不再需要时,必须使用 clearInterval, clearTimeout 清除。
  • 未移除的事件监听器: addEventListener 绑定的事件,在 DOM 元素被移除或组件销毁时,最好使用 removeEventListener 移除。
  • 闭包不当使用: 闭包会捕获外部变量。如果闭包长期存在,其捕获的变量也无法被回收。
  • DOM 引用: 缓存 DOM 元素时,如果 DOM 元素在页面中被移除,而 JS 仍然持有其引用,也会造成内存泄漏。

5.2 优化数据结构 (Optimize Data Structures)

  • 避免大量重复的小对象: 考虑使用对象池或更紧凑的数据结构。
  • WeakMapWeakSet: 当需要将数据与对象关联,且不阻止对象被垃圾回收时,使用 WeakMap (key 必须是对象) 或 WeakSet (value 必须是对象)。

5.3 及时释放资源

  • 对不再使用的对象,解除其引用,使其成为垃圾回收的目标。
  • 大型数组或对象在不再需要时,可以将其设置为 null

6. 构建与部署优化 (Build & Deployment Optimization)

这部分主要通过打包工具完成,但与 JS 性能密切相关。

6.1 代码拆分 (Code Splitting)

  • 将应用代码拆分成多个小块(chunks),按需加载,减少首次加载时间。
    • 基于路由拆分。
    • 基于组件拆分。
    • Webpack 的 import() 动态导入。

6.2 Tree Shaking

  • 移除项目中未被使用的代码("死代码"),减小最终打包文件体积。

6.3 Scope Hoisting (作用域提升)

  • Webpack 4 引入的优化,将多个模块合并到同一个作用域,减少函数包装,提升执行速度。

7. 性能分析工具 (Performance Analysis Tools)

  • 浏览器开发者工具:
    • Performance (性能) 面板: 记录页面加载和运行时的 CPU、网络、渲染活动。
    • Memory (内存) 面板: 检查内存使用情况,查找内存泄漏。
    • Network (网络) 面板: 分析网络请求,瀑布流图。
    • Lighthouse: 自动化工具,提供页面性能、可访问性、SEO 等方面的报告和改进建议。
  • WebPageTest: 专业的 Web 性能测试工具,提供多地点、多浏览器测试。
  • Webpack Bundle Analyzer: 可视化打包后的文件构成,帮助分析和优化打包体积。