1.降低 DOM 操作的开销
文档对象模型 (DOM) 是 Web 开发的重要基石,但过多的或不当的 DOM 操作会导致性能的严重损耗。所以降低 DOM 操作的开销是我们优化性能首要做的事。
降低代码的 DOM 操作的开销我们可以从下面三个方向入手
-
精简选择器:选择进行 DOM 操作时,应优先使用最有效的选择器。例如,getElementById() 通常比 querySelector() 性能更好。
建议:
- 尽可能使用 ID 选择器。
- 避免使用通配符选择器,例如
*和**。 - 优先使用元素类名而不是属性选择器。
- 仅在必要时使用后代选择器。
-
批量处理 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);
- 使用虚拟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 函数会自动遍历数组中的每个元素,并将其平方后的值存储到新的数组中。这样,就避免了重复计算和操作,从而帮助我们提高代码的性能。
- 避免不必要的计算
建议:
- 存储计算值: 如果计算结果不会发生变化,可以将结果存储起来,避免重复计算。例如,如果要计算一个常数的平方,可以将平方结果存储为常量,而不是每次都重新计算。
- 使用缓存: 缓存可以将计算结果存储在内存中,以便快速访问。例如,如果要频繁地访问同一个数据,可以将其缓存起来,避免每次都从数据库或网络中获取。
- 使用预处理: 预处理是指在程序运行之前进行一些计算或处理,以便程序运行时可以节省时间。例如,如果要对一个大型文本文件进行分析,可以先对文本文件进行预处理,将文本分割成单词或句子,并生成索引,以便分析时可以快速查找数据。