一、减少重绘和重排
(一)理解重绘和重排
- 重排(reflow)
- 当 DOM 的几何属性(如宽、高、位置等)发生变化时,浏览器需要重新计算元素的几何属性,这个过程就叫做重排。例如,当改变一个元素的宽度、高度、边距、填充或者位置等属性时,会触发重排。
- 频繁的重排会严重影响性能,因为浏览器需要重新计算页面布局,这是一个比较复杂的过程。
- 常见引起重排的操作包括添加或删除可见的 DOM 元素、改变元素位置、改变元素尺寸(如改变宽度、高度或内外边距)等。
- 重绘(repaint)
- 当元素的外观(如颜色、背景色等非几何属性)发生变化时,浏览器会对元素进行重绘。重绘的成本比重排要低,因为它不涉及布局的重新计算。
- 不过,如果重绘操作过于频繁,也会对性能产生一定的影响。
(二)减少重绘和重排的策略
- 合并样式修改
- 每次修改样式都会引起重排或重绘。如果可以将多个样式修改合并为一次操作,就能减少性能开销。
- 例如,不要这样写:
const element = document.getElementById('myElement')
element.style.width = '100px'
element.style.height = '100px'
element.style.backgroundColor = 'red'
const element = document.getElementById('myElement')
element.className = 'newStyle'
- 在 CSS 中定义
.newStyle类包含所需的样式:
.newStyle {
width: 100px;
height: 100px;
background - color: red;
}
- 离线操作 DOM
- 创建一个文档片段(DocumentFragment),在文档片段中进行 DOM 操作,最后将文档片段添加到实际的 DOM 中。这样可以将多次 DOM 操作合并为一次,减少重排次数。
- 例如,要创建一个包含多个列表项的无序列表:
const ul = document.createElement('ul')
const fragment = document.createDocumentFragment()
for (let i = 0
const li = document.createElement('li')
li.textContent = 'Item ' + i
fragment.appendChild(li)
}
ul.appendChild(fragment)
document.body.appendChild(ul)
- 避免频繁读取布局属性
- 当你读取一个元素的布局属性(如 offsetTop、offsetLeft、clientWidth 等)时,浏览器可能会为了返回最新的值而强制进行重排。如果在一个循环或者频繁调用的函数中读取这些属性,会导致大量不必要的重排。
- 例如,不要这样做:
function loop() {
for (let i = 0
const element = document.getElementById('myElement')
const offsetTop = element.offsetTop
// 其他操作
}
}
- 如果确实需要使用布局属性,可以先将属性值缓存起来,避免多次读取导致的重排:
const element = document.getElementById('myElement')
const offsetTop = element.offsetTop
function loop() {
for (let i = 0
// 使用缓存的offsetTop
// 其他操作
}
}
二、使用节流和防抖技术
(一)节流(throttle)
- 概念
- 节流的目的是在一定时间内,只允许函数执行一次。例如,当用户滚动页面时,我们可能希望每隔一段时间(如 200 毫秒)才执行一次滚动事件处理函数,而不是在滚动过程中不断地执行,这样可以避免过多的函数调用,减少性能开销。
- 实现方式
function throttle(func, delay) {
let timer = null;
return function () {
if (!timer) {
func.apply(this, arguments);
timer = setTimeout(() => {
timer = null;
}, delay);
}
};
}
- 例如,对于一个滚动事件处理函数,可以这样使用节流:
const throttledScrollHandler = throttle(function () {
console.log('Scrolled');
}, 200);
window.addEventListener('scroll', throttledScrollHandler);
(二)防抖(debounce)
- 概念
- 防抖是指在事件被触发后,等待一段时间(如 300 毫秒),如果在这段时间内事件没有再次被触发,才执行对应的函数。如果在等待时间内事件再次被触发,那么重新开始等待。
- 例如,在用户在搜索框中输入内容时,我们可能希望在用户停止输入一段时间后(比如 500 毫秒)才发送搜索请求,而不是用户每输入一个字符就发送请求。
- 实现方式
function debounce(func, delay) {
let timer = null;
return function () {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
func.apply(this, arguments);
}, delay);
};
}
- 例如,对于一个搜索框的输入事件处理函数,可以这样使用防抖:
const debouncedInputHandler = debounce(function () {
const inputValue = document.getElementById('searchInput').value;
console.log('Search for:', inputValue);
}, 500);
document.getElementById('searchInput').addEventListener('input', debouncedInputHandler);
三、使用性能分析工具
(一)浏览器开发者工具
- Chrome 开发者工具
- Performance 面板:可以记录一段时间内网页的性能指标,包括 CPU 使用率、内存占用、页面加载时间、脚本执行时间等。通过分析这些数据,可以找到性能瓶颈。例如,你可以看到哪些函数占用了大量的执行时间,是否有频繁的重排和重绘等。
- Timeline 面板:可以查看页面的加载和渲染过程,包括各个阶段(如解析 HTML、加载 CSS 和 JavaScript、渲染页面等)的时间消耗。它还可以显示事件(如鼠标点击、滚动等)与页面渲染之间的关系,帮助你定位性能问题。
- Profiler 面板(内存分析) :用于分析 JavaScript 内存使用情况。可以查看内存的分配和回收情况,帮助你发现内存泄漏问题。内存泄漏是指程序中已经不再使用的内存没有被及时释放,导致内存占用不断增加,最终可能会使程序崩溃。
- Firefox 开发者工具
- Performance 面板:与 Chrome 类似,提供了网页性能的详细分析。它可以记录页面加载、脚本执行、重排重绘等事件,并以可视化的方式展示时间轴和性能指标,方便开发者查找性能问题。
- Memory 面板:用于分析内存使用情况,包括堆内存(heap)和栈内存(stack)。可以查看对象的分配和释放情况,帮助检测内存泄漏和内存占用过高的问题。
(二)第三方性能分析工具
- Lighthouse
- 这是一个开源的自动化工具,用于提高网页的质量。它可以对网页进行性能、可访问性、最佳实践、搜索引擎优化(SEO)等多个方面的评估。
- Lighthouse 会生成一个详细的报告,指出网页在各个方面的得分和存在的问题。例如,在性能方面,它会给出页面加载时间、首次内容绘制(FCP)时间、最大内容绘制(LCP)时间等指标的评估,并提供优化建议,如优化图片、减少 JavaScript 阻塞等。
- WebPageTest
- 可以在不同的网络环境和浏览器下测试网页的性能。它可以模拟真实用户的访问情况,包括不同的带宽、延迟等网络条件。
- 该工具会生成详细的性能报告,包括页面加载的各个阶段的时间、资源加载情况等。通过比较不同测试条件下的性能结果,可以找到优化的方向。