如何优化我们的代码性能!!

269 阅读8分钟

1.降低 DOM 操作的开销

文档对象模型 (DOM) 是 Web 开发的重要基石,但过多的或不当的 DOM 操作会导致性能的严重损耗。所以降低 DOM 操作的开销是我们优化性能首要做的事。

降低代码的 DOM 操作的开销我们可以从下面三个方向入手

  1. 精简选择器:选择进行 DOM 操作时,应优先使用最有效的选择器。例如,getElementById() 通常比 querySelector() 性能更好。

    建议:

    • 尽可能使用 ID 选择器。
    • 避免使用通配符选择器,例如 ***
    • 优先使用元素类名而不是属性选择器。
    • 仅在必要时使用后代选择器。
  2. 批量处理 DOM 更改: 因为每次对 DOM 进行更改时,浏览器都会重新计算页面布局并重新绘制受影响的区域。为了最小化的重排和重绘,我通常建议批量处理 DOM 更改。例如,可以先将要添加或删除的所有元素收集到数组中,然后一次性将它们添加到或删除页面中。

    建议:

    • 在内存中缓存 DOM 更改,然后在一次操作中应用所有更改。
    • 使用文档碎片(DocumentFragment)来构建要添加到页面的新元素。
    • 使用 innerHTML 属性一次性更新元素的内容。

代码示例:

   // 使用DocumentFragment,批量处理DOM节点
   const fragment = document.createDocumentFragment();

   // 使用循环创建多个元素并添加到文档片段中
   for (let i = 0; i < 100; i++) {
       const element = document.createElement('div'); // 创建一个新的div元素
       fragment.appendChild(element); // 将新元素添加到文档片段中
   }

   // 将文档片段添加到DOM中,这样可以一次性添加所有元素,减少重排和重绘
   document.body.appendChild(fragment);
  1. 使用虚拟DOM或Web组件:我们可以像React或者vue一样使用虚拟DOM来最小化的直接操作DOM,这样可以大大提高性能。

高效的事件处理

错误的事件处理,会给我们的网站带来不必要的性能开销,导致性能缓慢和界面的不响应。所以优化事件处理是提供网站性能的一个很重要的步骤。

常见问题

在实际开发中,以下几种情况会导致事件处理效率低下:

  • 为每个元素单独绑定事件处理程序: 这种做法会带来大量的DOM操作,导致不必要的性能开销。
  • 过度使用事件监听器: 每个事件监听器都会占用一定的内存空间,过多监听器会导致内存泄漏。
  • 未对事件进行优化: 例如,未对事件进行节流或防抖处理,会导致事件处理函数被频繁触发,降低性能。

解决方案

1.事件委托

事件委托是通过利用DOM树的事件冒泡机制来达到减少事件处理程序数量的一种方案。具体来说,就是将事件处理程序绑定到父级元素,而不是每个子元素。当子元素触发事件时,事件会冒泡到父级元素,并由父级元素的事件处理程序进行处理。

场景举例:

假设我们有一个列表,每个列表项都有一个按钮。如果为每个按钮单独绑定点击事件处理程序,那么就会产生大量的DOM操作。而我们可以使用事件委托,将点击事件处理程序绑定到列表元素上。当用户点击任意按钮时,事件都会冒泡到列表元素,并由列表元素的点击事件处理程序进行处理。

代码示例:

    // 为父元素添加一个点击事件监听器,通过事件委托处理子元素的点击事件
    document.getElementById('parent').addEventListener('click', function(event) {
        // 检查点击的目标元素是否是一个按钮
        if (event.target.tagName === 'BUTTON') {
            console.log('Button clicked!'); // 如果是按钮,输出点击信息
        }
    });
2.节流和防抖

节流:节流是指限制事件处理函数的调用频率,避免其被频繁触发。例如,我们可以规定,事件处理函数在一定时间间隔内只会被调用一次,即使事件在该时间间隔内被多次触发,只会被调用一次。

防抖:防抖是指延迟事件处理函数的执行,直到事件停止触发。例如,我们可以规定,只有当事件停止触发一段时间后,事件处理函数才会被执行

节流函数代码示例:

    // 限制函数的执行频率
    function throttle(func, limit) {
        let lastFunc; // 保存上一次要执行的函数
        let lastRan; // 保存上一次函数执行的时间

        return function() {
            const context = this; // 保存上下文
            const args = arguments; // 保存参数

            if (!lastRan) {
                func.apply(context, args); // 第一次调用时立即执行
                lastRan = Date.now(); // 记录执行时间
            } else {
                clearTimeout(lastFunc); // 清除上一次的定时器
                lastFunc = setTimeout(function() {
                    // 在规定的时间间隔内再执行一次
                    if ((Date.now() - lastRan) >= limit) {
                        func.apply(context, args); // 执行函数
                        lastRan = Date.now(); // 更新执行时间
                    }
                }, limit - (Date.now() - lastRan)); // 设置定时器,保证时间间隔
            }
        }
    }

    // 使用节流函数处理窗口大小调整事件,限制执行频率为200毫秒
    window.addEventListener('resize', throttle(function() {
        console.log('Resize event'); // 每隔200毫秒输出一次
    }, 200));

防抖函数代码示例:

// 定义一个防抖函数 debounce,接受两个参数:要执行的函数 fn 和延迟时间 delay(毫秒)
function debounce(fn, delay) {
  // 初始化一个定时器变量 timer,初始值为 null
  let timer = null;

  // 返回一个新的函数,这个函数会在特定时间间隔后执行传入的函数 fn
  return function () {
    // 如果 timer 不为 null,表示之前已经有一个定时器在等待执行,则清除该定时器
    if (timer) {
      clearTimeout(timer);
    }

    // 重新设置一个新的定时器,在指定的延迟时间后执行传入的函数 fn
    timer = setTimeout(() => {
      // 使用 apply 方法调用传入的函数 fn,并将当前上下文(this)和参数传递过去
      fn.apply(this, arguments);
    }, delay); // 设置延迟时间为传入的 delay
  };
}

// 获取页面中的按钮元素
const button = document.querySelector('button');

// 为按钮添加一个点击事件监听器,使用防抖函数包装后的处理函数
button.addEventListener('click', debounce(() => {
  // 当按钮被点击时,输出 'click' 到控制台
  console.log('click');
}, 500)); // 设置防抖延迟时间为 500 毫秒

优化循环和逻辑

JavaScript 的性能常常受制于低效的代码结构,特别是循环和复杂的逻辑。为了提升代码性能,我们需要优化循环和逻辑,减少不必要的计算和操作。

  • 优化循环性能

建议:

  • 减少循环内部的工作量: 在循环内部,尽量减少需要执行的操作。例如,如果要对数组中的每个元素进行处理,可以将处理逻辑封装成函数,并在循环中调用函数。
  • 缓存循环长度: 如果循环要遍历的集合长度不会发生变化,可以将长度缓存起来,避免每次循环都重新计算长度。
  • 避免在循环中进行高成本操作: 在循环中尽量避免进行复杂计算或耗时的操作,例如网络请求、文件读写等。

代码举例:

低效的循环:

const numbers = [1, 2, 3, 4, 5]; // 定义一个包含数字的数组 
for (let i = 0; i < numbers.length; i++) { // 遍历数组,每次循环时检查数组长度
    const squared = numbers[i] * numbers[i]; // 计算数组中当前元素的平方
    console.log(squared); // 输出平方后的结果到控制台
}

优化后的循环:

const numbers = [1, 2, 3, 4, 5]; // 定义一个包含数字的数组
const squaredNumbers = numbers.map(number => number * number); // 使用 map 方法创建一个新的数组,其中包含每个元素的平方
console.log(squaredNumbers); // 输出新的数组到控制台

在第一个代码中,循环内部需要两次计算 numbers[i] 的值,一次用于获取元素,一次用于平方运算。此外,每次循环都需要重新计算 numbers.length 的值。

而在第二个代码中,map 函数会自动遍历数组中的每个元素,并将其平方后的值存储到新的数组中。这样,就避免了重复计算和操作,从而帮助我们提高代码的性能。

  • 避免不必要的计算

建议:

  • 存储计算值: 如果计算结果不会发生变化,可以将结果存储起来,避免重复计算。例如,如果要计算一个常数的平方,可以将平方结果存储为常量,而不是每次都重新计算。
  • 使用缓存: 缓存可以将计算结果存储在内存中,以便快速访问。例如,如果要频繁地访问同一个数据,可以将其缓存起来,避免每次都从数据库或网络中获取。
  • 使用预处理: 预处理是指在程序运行之前进行一些计算或处理,以便程序运行时可以节省时间。例如,如果要对一个大型文本文件进行分析,可以先对文本文件进行预处理,将文本分割成单词或句子,并生成索引,以便分析时可以快速查找数据。