js性能优化与调试技巧|青训营

93 阅读10分钟

1.减少DOM操作

每次进行 DOM 操作都需要重新计算布局和渲染,这是一个相对昂贵的操作,减少 DOM 操作可以减轻浏览器的负担,加快网页加载和响应速度。

1.1 批量DOM操作

将js多次修改DOM的操作合并为一次,比如使用文档片段

// 往ul中动态插入li节点
const fragment = document.createDocumentFragment()
const data = ['节点1', '节点2', '节点3']
data.forEach(item => {
    const li = document.createElement('li')
    li.textContent = item
    fragment.appendChild(li)
})
​
// 一次性插入
ul = document.getElementById('ul');
ul.appendChild(fragment);

image-20230826200131800.png

1.2 缓存DOM查询结果

如果要多次使用同一个 DOM 元素,可以将查询结果缓存起来,避免频繁地进行查询操作

// 将root节点保存到静态变量中,通过地址直接访问,减少root的频繁重复查询
const root = document.getElementById('root')
const one = root.getElementsByClassName('one')[0]
const two = root.getElementById('two')

1.3 减少重排重绘

  • 重绘:页面中某些元素的样式发生变化,需要重新渲染
  • 重排:页面布局发生变化,需要重新计算元素的位置和大小

通过js直接操控样式会导致浏览器重绘,所以最好不要直接通过element.style.backgroundColor='pink'这样的方式修改,可以将要修改的样式写成一个类,通过增减类的方式来控制页面变化,比如将颜色修改写成一个pink类,然后通过element.classList.add('pink')修改样式

通过js控制动画使得页面布局变化,从而引起浏览器的重排,所以最好不要使用js来实现动画,优先使用css实现,利用transformopacity 属性,比如:

<ul id="element"></ul>
<button id="button">点击</button>
/* 旋转45度效果 */
#element {
     width: 200px;
    height: 200px;
    transition: transform 0.3s ease;
}
​
.transformed {
    transform: rotate(45deg);
    opacity: 0.5;
}
// 按钮点击触发ul旋转
const btn = document.getElementById('button')
btn.addEventListener('click', function(){
  const element = document.getElementById('element')
  element.classList.add('transformed')
})

2.算法优化

2.1 尽早打破循环

通过在循环中尽早触发终止条件,来避免不必要的迭代,比如从一个数组中找目标元素,循环遍历的时候第一个就找到了,那后面就完全没有必要再执行了,可以直接break了,同样还有在函数执行的时候,也可以利用条件尽早return回去

2.2 使用快速失败

通过利用逻辑表达式中短路求值的特性,简单来说就是如果在一个逻辑表达式中前面部分就可以完全判断出整句的结果,后面就不再执行。这个特性大多数编程语言都存在,主要使用的是与或运算符号。当使用逻辑与(&&)运算符时,如果第一个条件为 false,则整个表达式的结果一定为 false,此时会立即停止计算后续的条件,并返回 false;当使用逻辑或运算符(||)时,如果第一个条件为 true,则整个表达式的结果一定为 true,此时会立即停止计算后续的条件,并返回 true

2.3 降低算法复杂度

通过选取合适的算法,来降低时间复杂度和空间复杂度,当然常规情况下可以直接使用js的一些内置函数,比如排序(sort),查找(find),过滤(filter)等等,但是在实际业务场景中也可能会有一些情况无法使用这些函数,比如后端返回了特别大的数据量,使用js自带的函数处理效率就不太够了,为了降低js的运行成本防止页面卡顿,就需要依据算法的复杂度来优化算法

3.节流防抖

节流防抖用于限制事件的触发频率,以减少不必要的代码执行。通常在处理用户输入、滚动事件等需要频繁触发的场景下使用

3.1 节流

事件被限制为一定时间间隔内只能触发一次。如果在这个时间间隔内多次触发事件,只有第一次会被处理,后续的触发会被忽略。这样可以确保代码逻辑以一定的速率执行,并减少不必要的计算和渲染

function throttle(func, delay) {
  let timerId;
  return function() {
    if (!timerId) {
      timerId = setTimeout(() => {
        func.apply(this, arguments);
        timerId = null;
      }, delay);
    }
  };
}
​
// 使用节流技术来限制函数执行的频率
const throttledFn = throttle(function() {
  // 这里是要执行的代码逻辑
}, 1000);
​
// 在触发事件时调用节流函数
window.addEventListener('scroll', throttledFn);

3.2 防抖

事件的触发会被推迟到一定时间后才执行。如果在这个时间间隔内多次触发事件,只有最后一次触发会生效,前面的触发将被忽略。这样可以确保代码逻辑只在事件触发结束后执行一次

function debounce(func, delay) {
  let timerId;
  return function() {
    clearTimeout(timerId);
    timerId = setTimeout(() => {
      func.apply(this, arguments);
    }, delay);
  };
}
​
// 使用防抖技术来延迟函数执行
const debouncedFn = debounce(function() {
  // 这里是要执行的代码逻辑
}, 1000);
​
// 在触发事件时调用防抖函数
inputElement.addEventListener('input', debouncedFn);

3.3 使用场景

节流可以理解为一段时间内只触发一次,防抖可以理解为连续触发只有最后一次生效,因此它们的常用使用场景如下:

节流:

  • 用户输入:在用户输入框中实时进行搜索或过滤操作时,可以使用节流来控制搜索请求的频率,避免过多的请求发送给服务器
  • 页面滚动:在处理滚动事件时,可以使用节流来限制执行频率,以减少滚动事件的处理次数,提升性能和滚动的平滑度
  • 窗口调整:当窗口大小调整时,可以使用节流来限制触发频率,以避免过多的重新计算和布局操作

防抖:

  • 按钮点击:在按钮点击事件处理中,可以使用防抖来避免用户频繁点击造成的多次触发,确保只有最后一次点击会被处理
  • 输入框校验:在用户输入文字后进行实时校验时,可以使用防抖来延迟校验操作,避免在用户连续输入时频繁触发校验逻辑
  • 搜索建议:在搜索框中提供搜索建议时,可以使用防抖来延迟发送请求并获取建议结果,减少请求次数和服务器负载

4.异步执行

js是单线程的语言,在执行任何耗时操作时,会阻塞其他代码的执行,从而会导致页面卡顿,加载缓慢,通过异步执行,可以将耗时操作移出主线程,避免阻塞其他代码的执行,提高整体性能。比如,当需要加载多个资源(图片、样式表、脚本)时,可以使用异步加载,同时请求多个资源,而不必等待前一个请求完成,这样页面就不会出现卡顿

5.通过原型新增方法

通过原型新增方法的方式,可以将方法定义在原型对象上,而不是每个实例对象上。这样所有的实例对象共享同一个方法,避免了每个实例对象都创建一份方法的开销,从而减少内存占用。同时还可以提高代码的执行效率,因为原型链是一条链接起来的对象链,js在查找对象属性时会按照原型链从下往上进行查找。如果方法定义在实例对象上,每次调用该方法都需要先在实例对象上查找,然后再沿着原型链查找,效率较低。而将方法定义在原型对象上,则可以直接通过原型链访问,省去了额外的查找操作

// 定义构造函数
function Person(name, age) {
  this.name = name;
  this.age = age;
}
​
// 在原型对象上新增方法
Person.prototype.sayHello = function() {
  console.log('Hello, my name is ' + this.name);
};
​
// 创建实例对象
const person1 = new Person('Alice', 25);
const person2 = new Person('Bob', 30);
​
person1.sayHello(); // 输出:Hello, my name is Alice
person2.sayHello(); // 输出:Hello, my name is Bob

6.使用事件委托

事件委托通过将事件处理程序附加到父级元素上,然后利用事件冒泡机制来处理子元素的事件,从而减少事件处理程序的数量,减少了内存占用和性能开销

<ul id="parent-list">
  <li>Item 1</li>
  <li>Item 2</li>
  <li>Item 3</li>
  <li>Item 4</li>
  <li>Item 5</li>
</ul>
const parentList = document.getElementById('parent-list');
​
// 监听父级元素的点击事件
parentList.addEventListener('click', function(event) {
  // 检查触发事件的元素是否为 li 元素
  if (event.target.tagName === 'li') {
    // 处理 li 元素的点击事件
    console.log(event.target.textContent);
  }
});
​

7.使用性能分析工具

7.1 Lighthouse

Lighthouse是Google Chrome 推出的一款开源自动化工具,它可以搜集多个现代网页性能指标,分析 Web 应用的性能并生成报告。可以在浏览器中打开控制台直接使用,通过测试网页,它会得出一些评估指标:

  1. 性能指标(Performance):衡量网页加载和交互速度的指标。它基于各种性能度量,如首次内容绘制(FCP)、最大内容绘制(LCP)、首次输入延迟(FID)等来评估网页的整体性能。更好的性能得分表示网页加载更快、交互更流畅
  2. 可访问性(Accessibility):评估网页在可用性方面是否具备无障碍特性,以确保所有用户,包括残障用户,都能够访问和使用网页。这包括检查网页是否有正确的语义结构、合适的标记、键盘导航支持、对色盲用户友好等
  3. 最佳实践(Best Practices):检查网页是否遵循 Web 开发的最佳实践。这包括检查是否使用了最新的安全协议(例如 HTTPS)、是否遵循 HTML、CSS 和 JavaScript 的最佳编码规范,以及是否采用了优化技术,如缓存策略和压缩资源等
  4. 搜索优化(SEO):评估网页在搜索引擎排名方面的表现。它检查网页是否有有意义的标题和描述标签、友好的 URL 结构、适当的标头标记等,以帮助搜索引擎理解和索引网页内容

7.2 Performance

同样在浏览器中打开控制台可以直接使用,提供以下功能:

  1. 导航计时:可以通过 window.performance.timing 对象获取与页面导航相关的性能指标。例如,可以获得从开始导航到各个关键事件(如重定向、DNS 查询、TCP 连接等)发生的时间间隔
  2. 资源计时:可以使用 window.performance.getEntriesByType('resource') 方法来获取与页面加载的各个资源(如图片、脚本、样式表等)相关的性能指标。这些指标包括资源的加载时间、传输时间、总时间等
  3. 用户计时:可以使用 window.performance.mark() 和 window.performance.measure() 方法来创建自定义的时间戳和测量点,以便更详细地了解代码的执行时间和效率
  4. 帧计时:可以使用 window.performance.getEntriesByType('frame') 方法来获取与浏览器的渲染帧相关的性能指标。这对于分析动画和滚动性能非常有用
  5. 内存计时:可以使用 window.performance.memory 对象获取浏览器内存的使用情况,包括已分配的内存、已使用的内存等

7.3 测试网页

以知乎的一篇专栏文章作为测试zhuanlan.zhihu.com/p/72370235

image-20230826215650572.png

image-20230826220256548.png

通过Lighthouse分析报告的指标可以看出知乎的专栏网页其他指标都处于一般水平,而SEO的评分非常高,说明知乎做了非常多的优化,通过查看该网页的html源码,发现确实如此,有非常详细的meta标签内容,有较多的语义化标签,网页url结构基本一致等,这也确实符合知乎的定位