各位前端er们,晚上好!我是老码,今天咱们来聊聊JavaScript的DOM优化,重点是如何减少回流(reflow)和重绘(repaint)。这俩兄弟可是性能大户,一不小心就会让你的页面卡成PPT。所以,优化它们,刻不容缓!
一、什么是回流(Reflow)和重绘(Repaint)?
想象一下,你装修房子,改了墙的颜色,这叫重绘。但如果你把墙拆了重新砌,那可是回流了!
-
重绘(Repaint): 元素样式的改变并不影响它在文档流中的位置(例如:
color
、background-color
、visibility
),浏览器只需重新绘制受影响的部分。简单来说,就是换个颜色,刷个漆。 -
回流(Reflow): 元素的尺寸、布局、或内容发生改变,导致文档流重新计算,影响到其他元素的布局,浏览器需要重新构建渲染树。这可是个大工程,类似于推倒重来,性能消耗巨大。常见的触发回流的操作包括:
- 改变窗口大小(resize)
- 改变字体(font-size)
- 添加或删除可见的DOM元素
- 改变元素的位置(position)
- 改变元素的尺寸(margin、padding、border、width、height)
- 内容改变,例如输入框输入文字
- 读取某些DOM属性(offsetLeft、offsetTop、offsetWidth、offsetHeight、scrollTop、scrollLeft、scrollWidth、scrollHeight、clientTop、clientLeft、clientWidth、clientHeight、getComputedStyle()等等)
记住,回流必定会引起重绘,而重绘不一定会引起回流。回流就像多米诺骨牌,一个倒下,一片跟着倒。所以,我们的目标是尽量避免回流!
二、回流和重绘的代价
每次回流,浏览器都要重新计算整个页面或者部分页面的几何信息,这非常消耗资源。重绘虽然代价小一些,但频繁的重绘也会影响用户体验。如果你的页面动画不流畅,或者响应迟缓,很可能就是回流和重绘在作祟。
三、如何减少回流和重绘?
现在,进入正题,也是大家最关心的部分:如何减少回流和重绘?记住,我们的目标是:减少不必要的回流,让浏览器更轻松地工作。
1. 集中修改样式
避免频繁地直接修改DOM元素的样式,最好一次性修改。
-
错误示范:
let element = document.getElementById('myElement'); element.style.width = '100px'; // 回流 element.style.height = '200px'; // 回流 element.style.backgroundColor = 'red'; // 重绘
每次修改样式都会触发回流或重绘,性能很差。
-
正确示范:
-
使用CSS类名: 先定义好CSS类,然后通过修改元素的类名来批量修改样式。
// CSS .newStyle { width: 100px; height: 200px; background-color: red; } // JavaScript element.classList.add('newStyle'); // 只回流一次
-
使用CSS文本: 直接修改
style.cssText
属性,一次性设置所有样式。element.style.cssText = 'width: 100px; height: 200px; background-color: red;'; // 只回流一次
-
使用
setProperty
:
element.style.setProperty('width', '100px'); element.style.setProperty('height', '200px'); element.style.setProperty('backgroundColor', 'red');
-
2. 批量修改DOM
避免频繁地添加、删除DOM元素,尽量批量操作。
-
错误示范:
let container = document.getElementById('myContainer'); for (let i = 0; i < 100; i++) { let newElement = document.createElement('div'); newElement.textContent = 'Item ' + i; container.appendChild(newElement); // 每次appendChild都会触发回流 }
-
正确示范:
-
使用DocumentFragment: 先将所有DOM操作放到
DocumentFragment
中,然后一次性添加到页面。DocumentFragment
是一个轻量级的Document
对象,它存在于内存中,不属于文档树,因此对其进行操作不会触发回流和重绘。let container = document.getElementById('myContainer'); let fragment = document.createDocumentFragment(); // 创建文档片段 for (let i = 0; i < 100; i++) { let newElement = document.createElement('div'); newElement.textContent = 'Item ' + i; fragment.appendChild(newElement); // 将元素添加到文档片段 } container.appendChild(fragment); // 一次性添加到页面,只回流一次
-
使用innerHTML(谨慎): 虽然
innerHTML
简单粗暴,但如果只是添加简单的文本内容,性能还不错。但是,对于复杂的HTML结构,还是建议使用DocumentFragment
,因为innerHTML
会重新解析HTML字符串,可能会有安全风险。let container = document.getElementById('myContainer'); let html = ''; for (let i = 0; i < 100; i++) { html += '<div>Item ' + i + '</div>'; } container.innerHTML = html; // 可能会回流,取决于浏览器优化
-
3. 缓存DOM信息
避免频繁读取DOM元素的属性,例如offsetWidth
、offsetHeight
等。每次读取这些属性,浏览器为了保证数据的准确性,都会强制进行回流。
-
错误示范:
let element = document.getElementById('myElement'); for (let i = 0; i < 100; i++) { let width = element.offsetWidth; // 每次循环都触发回流 // ...其他操作 }
-
正确示范:
let element = document.getElementById('myElement'); let width = element.offsetWidth; // 只读取一次 for (let i = 0; i < 100; i++) { // 使用缓存的width // ...其他操作 }
4. 离线修改DOM
如果需要对DOM元素进行大量的修改,可以先将元素从文档流中移除,修改完成后再放回去。
-
方法一:
display: none
先将元素设置为
display: none
,这样对元素的操作不会触发回流,修改完成后再设置为display: block
。let element = document.getElementById('myElement'); element.style.display = 'none'; // 移除文档流 // ...进行大量修改 element.style.display = 'block'; // 恢复文档流,触发一次回流
-
方法二:使用
cloneNode
克隆一个节点,修改克隆节点,然后用克隆节点替换原始节点。
let element = document.getElementById('myElement'); let clone = element.cloneNode(true); // 深拷贝 // ...对clone进行大量修改 element.parentNode.replaceChild(clone, element); // 替换原始节点,触发一次回流
5. 避免使用Table布局
Table布局非常容易触发回流,因为Table中一个元素的改变可能会影响到整个Table的布局。尽量使用CSS布局(Flexbox、Grid)代替Table布局。
6. 避免使用CSS表达式(Expression)
CSS表达式在每次页面重绘时都会重新计算,性能很差,尽量避免使用。
7. 优化动画
动画是回流和重绘的重灾区,优化动画至关重要。
-
使用
requestAnimationFrame
: 使用requestAnimationFrame
来执行动画,可以让浏览器在最佳的时机执行动画,避免掉帧。function animate() { // ...动画逻辑 requestAnimationFrame(animate); } requestAnimationFrame(animate);
-
使用
transform
和opacity
: 尽量使用transform
和opacity
来实现动画,因为它们不会触发回流,只会触发重绘。element.style.transform = 'translate(100px, 100px)'; // 只触发重绘 element.style.opacity = 0.5; // 只触发重绘
为什么
transform
和opacity
性能更好?因为
transform
和opacity
的改变通常由GPU处理,而不是CPU。GPU更擅长处理图形渲染,因此性能更好。
8. 合理使用CSS选择器
CSS选择器的效率也会影响性能。尽量使用ID选择器和类选择器,避免使用复杂的选择器,例如通配符选择器(*
)和属性选择器([attribute]
)。
9. 避免频繁的DOM操作
这是最重要的一点。任何DOM操作都可能触发回流和重绘,所以尽量减少DOM操作。
10. 利用Shadow DOM
Shadow DOM 可以将一个 DOM 节点与其子节点隔离,从而避免了全局 CSS 样式对这些节点的影响。这意味着,当 Shadow DOM 中的元素发生变化时,不会影响到页面上的其他元素,从而减少了回流和重绘的范围。
11. 使用Web Workers
Web Workers 允许你在后台线程中执行 JavaScript 代码,而不会阻塞主线程。你可以将一些耗时的 DOM 操作放到 Web Workers 中执行,从而避免了主线程的卡顿。
总结:减少回流和重绘的黄金法则
法则 | 描述 | 示例 |
---|---|---|
集中修改样式 | 将多个样式修改合并为一个操作。 | 使用CSS类名、cssText 或setProperty 。 |
批量修改DOM | 使用DocumentFragment 或innerHTML 批量添加DOM元素。 |
创建DocumentFragment ,将多个元素添加到DocumentFragment ,然后一次性添加到DOM。 |
缓存DOM信息 | 避免重复读取DOM属性,将结果缓存起来。 | 将offsetWidth 、offsetHeight 等属性缓存到变量中。 |
离线修改DOM | 先将元素从文档流中移除,修改完成后再放回去。 | 使用display: none 或克隆节点。 |
避免Table布局 | 尽量使用CSS布局代替Table布局。 | 使用Flexbox或Grid布局。 |
避免CSS表达式 | 避免使用CSS表达式。 | 使用JavaScript代替CSS表达式。 |
优化动画 | 使用requestAnimationFrame 、transform 和opacity 。 |
使用requestAnimationFrame 执行动画,使用transform 进行位移、旋转、缩放,使用opacity 改变透明度。 |
合理使用CSS选择器 | 使用ID选择器和类选择器,避免使用复杂的选择器。 | 尽量避免使用通配符选择器和属性选择器。 |
减少DOM操作 | 这是最重要的一点。 | 尽量避免不必要的DOM操作。 |
利用Shadow DOM | 将 DOM 节点与其子节点隔离,避免全局 CSS 样式对这些节点的影响。 | 使用 Shadow DOM 创建独立的组件。 |
使用Web Workers | 将耗时的 DOM 操作放到 Web Workers 中执行,避免阻塞主线程。 | 将数据处理和复杂的计算放到 Web Workers 中。 |
四、工具与技巧
-
浏览器开发者工具: 现代浏览器都提供了强大的开发者工具,可以帮助你分析页面的性能瓶颈。通过Timeline或Performance面板,你可以清晰地看到回流和重绘的发生情况,以及它们所消耗的时间。
-
Lighthouse: Google的Lighthouse是一个开源的自动化工具,可以用来审计网页的性能、可访问性、最佳实践等。它可以帮助你发现潜在的性能问题,并提供优化建议。
-
Performance API: JavaScript提供了Performance API,可以让你更精确地测量页面的性能指标,例如回流和重绘的时间。
五、案例分析
假设我们有一个需求:实现一个列表的动态排序。
-
传统方法(容易触发回流):
每次点击排序按钮,都重新生成整个列表的DOM结构。
-
优化方法:
- 先对数据进行排序。
- 使用
DocumentFragment
批量更新列表的DOM结构。
通过这种方式,可以大大减少回流的次数,提高性能。
六、总结
减少回流和重绘是一个持续优化的过程,没有一劳永逸的解决方案。我们需要根据具体的场景,选择合适的优化策略。记住,代码优化没有银弹,只有不断学习和实践,才能写出高性能的JavaScript代码。
希望今天的分享对大家有所帮助!记住,优化性能,就是优化用户体验! 感谢大家! 下课!