JavaScript 性能优化
性能优化是一个持续的过程,旨在提升用户体验、减少资源消耗、提高应用响应速度。在 JavaScript 中,优化不仅仅意味着写更快的代码,还包括如何高效地与浏览器、网络和用户交互。
1. 性能优化基本原则 (The Golden Rules)
无论何种场景,性能优化都遵循以下几个核心原则:
- 减少 (Reduce): 减少代码量、减少资源大小、减少 HTTP 请求、减少 DOM 操作。
- 延迟 (Defer): 延迟加载不必要的资源,延迟执行非关键代码。
- 优化 (Optimize): 优化算法、优化数据结构、优化渲染流程、利用缓存。
- 测量 (Measure): 没有测量就没有优化。在优化前后进行性能测试,确保改进有效。
- 不作不必要优化: 过早优化是万恶之源。只优化瓶颈,而非所有代码。
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)。
- 选择合适的数据结构:
MapvsObject: 当键不是字符串或需要保持插入顺序时,Map性能更好。SetvsArray:Set在查找(has)和去重方面比Array快得多(O(1) vs O(n))。
- 缓存计算结果 (Memoization): 对于计算成本高且输入参数不变的函数,缓存其结果。
2.4 避免全局变量和隐式全局变量
- 过多的全局变量会增加命名冲突的风险,更重要的是,它们会延长变量的生命周期,可能导致内存泄漏。
- 使用
var声明但未在函数内部声明的变量会自动成为全局变量。始终使用const或let。
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: absolute或fixed,减少对其他元素的影响。 - 使用 CSS Transform 和 Opacity: 这些属性通常由 GPU 加速,不会触发布局或绘制。
- 使用
will-change属性: 提前告知浏览器哪些属性将发生变化,浏览器可以进行优化。 - 合理使用
display: none: 隐藏元素会触发一次回流,但之后的操作不会影响布局,直到再次显示。
- 脱离文档流: 复杂动画使用
4.3 动画优化 (Animation Optimization)
- 优先使用 CSS 动画/过渡: 浏览器可以对其进行优化,并将其放在独立的线程上执行,不阻塞 JS 主线程。
- 使用
requestAnimationFrame()(rAF): 实现 JS 驱动的动画。rAF会在浏览器下一次重绘之前调用回调函数,确保动画与浏览器刷新率同步,避免丢帧。jsfunction 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)
- 避免大量重复的小对象: 考虑使用对象池或更紧凑的数据结构。
WeakMap和WeakSet: 当需要将数据与对象关联,且不阻止对象被垃圾回收时,使用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: 可视化打包后的文件构成,帮助分析和优化打包体积。