优化Javascript代码来提高性能 | 青训营

128 阅读11分钟

JavaScript编码的原则

JavaScript编码的原则可以总结为各司其责、组件封装和过程抽象。

  • 各司其责:在JavaScript编码中,应该尽量避免不必要地使用 JavaScript 直接操作样式。通过使用 CSS 类来表示状态和样式,可以更好地分离结构(HTML)和样式(CSS)的责任。

  • 组件封装:是指将相关的功能和界面封装成可复用的组件,以提高代码的重用性和可维护性。在JavaScript中,可以使用对象、类或者模块来实现组件封装。

  • 过程抽象:是指将复杂的操作封装成简单的步骤或者函数,以提高代码的可读性和可维护性。在JavaScript中,可以使用函数来实现过程抽象。

如何通过优化Javascript代码来提高性能

怎么理解回流跟重绘?什么场景下会触发?

HTML中,每个元素都可以理解成一个盒子,在浏览器解析过程中,会涉及到回流与重绘。回流和重绘都是在渲染过程中发生的,它们都会影响网页的性能和用户体验。下面我们来看看它们分别是什么,以及有什么区别:

  • 回流:布局引擎会根据各种样式计算每个盒子在页面上的大小与位置(回流也叫重排)。 当一个元素的布局或者几何属性发生改变时(比如改变宽高、边距、定位、显示隐藏等),浏览器就会对该元素及其子元素、后续元素、甚至整个页面进行回流,重新计算它们在文档流中的位置和大小,并重新构建渲染树。回流会改变渲染树的结构,也会影响元素的外观。

  • 重绘:当计算好盒模型的位置、大小及其他属性后,浏览器根据每个盒子特性进行绘制。 当一个元素的外观发生改变,但不影响它在文档流中的位置时(比如改变背景色、字体颜色、边框颜色等),浏览器就会对该元素进行重绘,重新生成像素并更新屏幕上的内容。重绘不会改变渲染树的结构,只会影响元素的外观。

回流的触发条件:

回流触发条件

简单来说,就是当我们对 DOM 结构的修改引发 DOM 几何尺寸变化的时候,会发生回流的过程。比如以下情况:

1、一个 DOM 元素的几何属性变化,常见的几何属性有width、height、padding、margin、left、top、border 等等, 这个很好理解。

2、使 DOM 节点发生增减或者移动。

3、读写 offset族、scroll族和client族属性的时候,浏览器为了获取这些值,需要进行回流操作。

4、调用 window.getComputedStyle 方法。

重绘触发条件

触发回流一定会触发重绘

可以把页面理解为一个黑板,黑板上有一朵画好的小花。现在我们要把这朵从左边移到了右边,那我们要先确定好右边的具体位置,画好形状(回流),再画上它原有的颜色(重绘)

除此之外还有一些其他引起重绘行为:

  • 颜色的修改
  • 文本方向的修改
  • 阴影的修改

从上面的定义可以看出,回流比重绘更耗费性能,因为回流涉及到更多的计算和渲染。而且,一次回流往往会引起一次或多次重绘,但重绘不一定会导致回流。所以,在优化网页性能时,我们应该尽量减少回流和重绘的次数和范围。

减少页面的重绘(Repaint)和重排(Reflow)

当DOM元素的样式属性发生变化时,浏览器会执行重绘(repaint)和重排(reflow)操作,这些操作消耗了大量的性能。为了最小化这些操作,开发者可以采取以下措施:

  1. 批量修改样式:避免在循环中直接修改元素的样式属性,而是将多个样式的修改批量地应用到元素,或使用CSS类来一次性修改多个样式。
// 避免频繁修改样式
const element = document.getElementById('myElement');
element.style.width = '100px';
element.style.height = '100px';
element.style.backgroundColor = 'red';

// 批量修改样式
element.style.cssText = 'width: 100px; height: 100px;';

// 使用 CSS 类名一次性修改样式 
element.classList.add('red-box');

  1. 使用CSS的transform和opacity:对于动画和元素的缩放等操作,使用CSS的transform属性而非直接修改元素的位置和尺寸,以减少重排和重绘的次数。
// 避免频繁的位置和尺寸修改
element.style.left = '100px';
element.style.top = '100px';
element.style.width = '200px';
element.style.height = '200px';

// 使用CSS的transform属性
element.style.transform = 'translate(100px, 100px) scale(2)';
  1. 使用文档片段(DocumentFragment)进行插入操作:通过使用文档片段,将多个节点插入到文档中,从而减少重排的次数。
const fragment = document.createDocumentFragment();

for (let i = 0; i < 100; i++) {
  const element = document.createElement('div');
  element.textContent = 'Item ' + i;
  fragment.appendChild(element);
}

document.getElementById('container').appendChild(fragment);
  1. 优化DOM操作:减少对DOM的频繁操作,通过缓存元素的引用避免多次访问DOM节点。将元素离线(脱离文档流)、批量操作和使用文档片段(DocumentFragment)等方法来减少重排和重绘.
// 避免频繁的DOM操作
for (let i = 0; i < array.length; i++) {
  document.getElementById('container').innerHTML += '<div>' + array[i] + '</div>';
}

// 批量操作DOM
const container = document.getElementById('container');
const fragment = document.createDocumentFragment();
for (let i = 0; i < array.length; i++) {
  const div = document.createElement('div');
  div.textContent = array[i];
  fragment.appendChild(div);
}
container.appendChild(fragment);

重绘和重排是比较昂贵的操作,尽量使用优化方法将其降到最低可以有效提高网页性能。

使用节流和防抖技术

当需要优化JavaScript代码的性能时,可以使用节流(Throttling)和防抖(Debouncing)技术来限制函数的执行频率,避免过多的函数调用。

  1. 节流技术实例: 节流技术通过在一定时间间隔内限制函数的执行次数,可以在用户频繁触发事件时提高性能。
function throttle(func, delay) {
  let timerId;
  return function() {
    if (!timerId) {
      timerId = setTimeout(() => {
        func.apply(this, arguments);
        timerId = null;
      }, delay);
    }
  };
}

// 示例:在滚动事件中使用节流技术
window.addEventListener(
  'scroll',
  throttle(function() {
    console.log('Scroll event');
  }, 200)
);
  1. 防抖技术实例: 防抖技术通过在事件触发后等待一定时间间隔后执行函数,以确保执行函数的频率不会过高。
function debounce(func, delay) {
  let timerId;
  return function() {
    clearTimeout(timerId);
    timerId = setTimeout(() => {
      func.apply(this, arguments);
    }, delay);
  };
}

// 示例:在输入框输入事件中使用防抖技术
const input = document.getElementById('input');
input.addEventListener(
  'input',
  debounce(function() {
    console.log('Input event');
  }, 300)
);

上述代码中,节流和防抖函数会返回一个新的函数,该新函数在一定时间间隔内限制了原始函数的执行次数。通过将节流函数应用于滚动事件,或将防抖函数应用于输入框输入事件等场景中,可以减少函数的执行次数,提高性能。

使用节流和防抖技术可以有效控制函数的频繁调用,避免过多的计算和操作,以提升JavaScript代码的性能。根据实际需求选择适合的技术以达到优化性能的目的。

事件委托

事件委托是一种优化 JavaScript 性能的技术,它通过将事件处理程序绑定到父元素上,而不是每个子元素上,从而减少了事件处理程序的数量。这样可以减少内存消耗和提高页面响应速度。 下面是一个使用事件委托进行性能优化的实例:

HTML 代码:

<ul id="list">
  <li>Item 1</li>
  <li>Item 2</li>
  <li>Item 3</li>
  <li>Item 4</li>
  <li>Item 5</li>
</ul>

JavaScript 代码:

// 获取父元素
var list = document.getElementById('list');

// 添加事件处理程序
list.addEventListener('click', function(event) {
  // 检查点击的元素是否为 li 元素
  if (event.target.tagName === 'LI') {
    // 执行相应的操作
    console.log('Clicked on:', event.target.textContent);
  }
});

在上面的代码中,我们将事件处理程序绑定到父元素 list 上,而不是每个子元素 li 上。当用户点击 li 元素时,事件会冒泡到父元素,并触发事件处理程序。然后我们可以通过 event.target 属性来获取实际被点击的元素。

使用事件委托的好处是,无论有多少个子元素,我们只需要一个事件处理程序。这样可以减少内存消耗,并且在动态添加或删除子元素时,不需要重新绑定事件处理程序。

总结:通过使用事件委托,我们可以优化 JavaScript 性能,减少事件处理程序的数量,提高页面响应速度。

异步操作

JavaScript 中使用异步操作可以提高代码的性能和用户体验。

  • 异步请求数据:使用 XMLHttpRequest 发起异步请求,并在请求完成后处理返回的数据。
// 使用 XMLHttpRequest 发起异步请求
var xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.example.com/data', true);
xhr.onreadystatechange = function() {
  if (xhr.readyState === 4 && xhr.status === 200) {
    var data = JSON.parse(xhr.responseText);
    // 处理返回的数据
    console.log(data);
  }
};
xhr.send();
  • 使用 Promise 处理异步操作:使用 Promise 来处理异步操作。通过返回一个 Promise 对象,我们可以使用 then() 方法来处理成功的情况,使用 catch() 方法来处理错误的情况。
// 使用 Promise 处理异步操作
function fetchData() {
  return new Promise(function(resolve, reject) {
    setTimeout(function() {
      var data = 'Some data';
      // 模拟异步操作
      if (data) {
        resolve(data);
      } else {
        reject('Error');
      }
    }, 2000);
  });
}

fetchData()
  .then(function(data) {
    // 处理返回的数据
    console.log(data);
  })
  .catch(function(error) {
    // 处理错误
    console.log(error);
  });
  • 使用 async/await 处理异步操作:使用 async/await 来处理异步操作。通过在函数前面加上 async 关键字,我们可以使用 await 关键字来等待异步操作的结果,并以同步的方式处理返回的数据。
// 使用 async/await 处理异步操作
async function fetchData() {
  return new Promise(function(resolve, reject) {
    setTimeout(function() {
      var data = 'Some data';
      // 模拟异步操作
      if (data) {
        resolve(data);
      } else {
        reject('Error');
      }
    }, 2000);
  });
}

async function getData() {
  try {
    var data = await fetchData();
    // 处理返回的数据
    console.log(data);
  } catch (error) {
    // 处理错误
    console.log(error);
  }
}

getData();

在上面的示例中,我们使用异步操作来处理网络请求和模拟耗时操作。通过使用异步操作,可以避免阻塞主线程,提高代码的性能和用户体验。

异步加载和懒加载:

异步加载资源: 使用 asyncdefer 属性加载脚本,使其不阻塞页面渲染。

图片懒加载: 只加载用户可视区域内的内容,延迟加载其他内容,提高页面初始加载速度。

<!-- 异步加载脚本 -->
<script src="app.js" async></script>

<!-- 图片懒加载 -->
<img src="placeholder.jpg" data-src="image.jpg" alt="Lazy-loaded Image" class="lazy-image">

性能调试

当遇到JavaScript性能问题时,我们可以使用以下调试技术和实例来识别和解决问题:

  1. 使用性能分析工具

    • Chrome Performance工具:在Chrome浏览器的开发者工具中,使用Performance选项卡可以进行性能分析。通过录制页面的加载和运行时间,我们可以查看函数执行的时间、CPU使用情况、内存占用等信息,从而找出性能瓶颈。

    • Lighthouse: Lighthouse 是一个用于评估网页性能的工具,可以从多个方面评估性能,如性能、可访问性、最佳实践等。

    • Webpack 分析工具: 如果你使用 Webpack 进行打包,Webpack 提供了分析工具可以帮助你查看打包后的文件大小和依赖关系。

  2. 使用console.log()和console.time()

    • 使用console.log()打印关键变量的值,检查是否符合预期,以帮助我们定位潜在的性能问题。
    • 使用console.time()和console.timeEnd()测量代码块的执行时间,以确定是否有耗时较长的操作。
  3. 使用性能检查

    • 使用console.profile()和console.profileEnd()命令在代码块中开始和结束性能检查,然后在浏览器的性能检查面板(如Chrome DevTools)中查看生成的性能报告。
  4. 监视内存使用

    • 使用console.memory命令监视内存使用情况,并查看内存泄漏等问题。
  5. 使用requestAnimationFrame()

    • 当使用动画或频繁重绘时,使用requestAnimationFrame()而不是setTimeout()或setInterval(),以优化性能并避免卡顿。

使用 Web Workers

Web Workers 允许在后台线程中执行 JavaScript 代码,避免阻塞主线程,提高页面响应性能。

// 创建一个 Web Worker  
const worker = new Worker('worker.js');  
  
// 监听消息事件  
worker.onmessage = function(event) {  
  console.log(event.data);  
};  
  
// 向 Worker 发送消息  
worker.postMessage('Hello from main thread!');  

缓存和优化网络请求:

使用缓存:  使用浏览器缓存来存储静态资源,减少重复的网络请求。

合并请求:  将多个小的网络请求合并为一个大的请求,减少网络开销。

我们为什么要关注性能呢?

  1. 性能是创建网页或应用程序时最重要的一个方面。
    没有人想要应用程序崩溃或者网页无法加载,或者用户的等待时间过长。根据有关统计,47%的访问者希望网站在不到 2 秒的时间内加载,如果加载过程需要 3 秒以上,则有 40%的访问者会离开网站。
  2. 当代用户对于应用的体验普遍要求提高了
    几年前,对于一个页面的等待时间,普遍数据是7s
    而现在,如果一个页面卡顿或者白屏时间长于2s,可能就会有一半的人选择离开,当然,政府的项目不在考量的范围以内,比如说什么社保系统,征信系统,就算等待时间超过一天,咱也只能等
  3. 应用的性能瓶颈,依然是在js上
    尤其是现在的三大框架,数据驱动视图,而数据的操作运算依赖于js,所以js的性能就显得更为重要了,对于一个js代码块儿,我们是能缩短一毫秒,那就缩短一毫秒

考虑到以上这些数字,你在创建 Web 应用程序时应始终考虑性能。

永远不要忽略代码优化工作,重构是一项从项目开始到结束需要持续的工作,只有不断的优化代码才能让代码的执行效率越来越好。