重排(回流 reflow)和重绘(repaint)
页面生成过程
图示过程
文字解释
-
HTML解析器将HTML解析成DOM树
-
CSS解析器将CSS解析成CSSOM树
-
结合两棵树生成一颗渲染树(Render Tree),该过程称为Attachment
-
浏览器根据布局方式,在屏幕上“画”出渲染树的所有节点
-
然后将布局绘制在屏幕上,显示出整个页面
-
第四步和第五步最耗时,合起来就是常说的渲染
重排(回流reflow)
概念
- 当DOM的变化影响了元素的几何信息(元素的的位置和尺寸大小),浏览器需要重新计算元素的几何属性,将其安放在界面中的正确位置,这个过程叫做重排。
- 简单的说就是重新生成布局,重新排列元素。
什么情况会发生重排?
-
页面初始渲染,这是开销最大的一次重排
-
增删可见的DOM元素
-
改变元素位置
-
改变元素尺寸:元素总尺寸=宽高(内容填充)+内边距+边框+外边距
-
改变元素内容:文字数量,图片大小等
-
改变元素字体大小
-
改变浏览器窗口尺寸:resize事件发生时
-
激活CSS伪类
-
增加CSS伪元素
-
设置 style 属性的值,每一次设置都会触发一次reflow
-
查询某些属性或调用某些计算方法:offsetWidth、offsetHeight等,除此之外,当我们调用
getComputedStyle
方法,或者IE里的currentStyle
时,也会触发重排,原理是一样的,都为求一个“即时性”和“准确性”。 -
常见引起重排属性和方法 — — — width height margin padding display border-width border position overflow font-size vertical-align min-height clientWidth clientHeight clientTop clientLeft offsetWudth offsetHeight offsetTop offsetLeft scrollWidth scrollHeight scrollTop scrollLeft scrollIntoView() scrollTo() getComputedStyle() getBoundingClientRect() scrollIntoViewIfNeeded()
重排影响范围
全局范围(避免)
- 从根节点html开始对整个渲染树进行重新布局。
局部范围
- 对渲染树的某部分或某一个渲染对象进行重新布局
- 将一个DOM元素的宽高等几何信息定死,在这个元素内部触发重排,只会重新渲染该元素内部的元素,不会影响到外界
重绘(repaint)
概念
- 当一个元素的外观发生改变,但没有改变布局,重新把元素外观绘制出来的过程。
什么情况下会发生重绘?
-
属性: — — — color border-style visibility background text-decoration background-image background-position background-repeat outline-color outline outline-style border-radius outline-width box-shadow background-size
重排和重绘比较
-
两者都会影响性能,但重排比重绘工作量大,会使我们付出高额的性能代价
-
重绘不一定导致重排,重排一定会导致重绘
-
重绘单单改变元素的外观,不会引起网页重新生成布局
-
当浏览器完成重排之后,将会重新绘制受到此次重排影响的部分。
-
优化方案
减少重排范围
- 尽量以局部布局的形式组织html结构,尽可能小的影响重排的范围(BFC)
- 不要使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局。那么在不得已使用table的场合,可以设置table-layout:auto;或者是table-layout:fixed这样可以让table一行一行的渲染,这种做法也是为了限制reflow的影响范围。
减少重排次数
1. 样式集中改变
-
对于页面来说,明智且可维护的做法是更改类名而不是修改样式。
-
利用style.cssText或者classname进行批量修改
-
document.getElementById("d1").style.cssText = "color:red; font-size:13px;"; document.getElementById("d1").classList.replace=('previousClassname','newClassname') 复制代码
-
2. 分离读写操作(避免强制刷新)
-
DOM 的多个读操作(或多个写操作),应该分别放在一起。不要两个读操作之间,加入一个写操作。
-
// bad 强制刷新 触发四次重排+重绘 div.style.left = div.offsetLeft + 1 + 'px'; div.style.top = div.offsetTop + 1 + 'px'; div.style.right = div.offsetRight + 1 + 'px'; div.style.bottom = div.offsetBottom + 1 + 'px'; // good 缓存布局信息 相当于读写分离 触发一次重排+重绘 var curLeft = div.offsetLeft; var curTop = div.offsetTop; var curRight = div.offsetRight; var curBottom = div.offsetBottom; div.style.left = curLeft + 1 + 'px'; div.style.top = curTop + 1 + 'px'; div.style.right = curRight + 1 + 'px'; div.style.bottom = curBottom + 1 + 'px'; 复制代码
-
为什么读写分离后只触发一次重排
-
这都得益于浏览器的渲染队列机制
-
当我们修改了元素的几何属性,导致浏览器触发重排或重绘时。它会把该操作放进渲染队列,等到队列中的操作到了一定的数量或者到了一定的时间间隔时,浏览器就会批量执行这些操作。
-
div.style.left = '10px'; div.style.top = '10px'; div.style.width = '20px'; div.style.height = '20px'; // 这时渲染队列有上面四个写操作 console.log(div.offsetLeft); // 在第一个console的时候,涉及div.offsetLeft关键样式,浏览器会立即执行渲染队列的任务,把之前上面四个写操作的渲染队列都给清空了。 // 因为队列中,可能会有影响到这些值的操作(即console.log操作要打印最新的div.offsetLeft的值),为了给我们最精确的值,浏览器会立即重排+重绘,所以队列全部清空。 // 注意: 涉及关键属性的操作,无论何时浏览器都会立即执行渲染队列的任务,即使该值与你操作中修改的值没关联。 console.log(div.offsetTop); console.log(div.offsetWidth); console.log(div.offsetHeight); // 剩下的console,因为渲染队列本来就是空的,所以并没有触发重排,仅仅拿值而已。 复制代码
-
强制刷新队列的操作请求
- offsetTop, offsetLeft, offsetWidth, offsetHeight
scrollTop, scrollLeft, scrollWidth, scrollHeight
clientTop, clientLeft, clientWidth, clientHeight
getComputedStyle(), 或者 IE的 currentStyle
- offsetTop, offsetLeft, offsetWidth, offsetHeight
-
3. 将要修改的DOM元素离线操作
1. 使用 display:none
- 一旦我们给元素设置
display:none
时(只有一次重排重绘),元素便不会再存在在渲染树中,相当于将其从页面上“拿掉”,我们之后的操作将不会触发重排和重绘,添加足够多的变更后,通过display
属性显示(另一次重排重绘),通过这种方式即使大量变更也只触发两次重排。
2. 创建碎片
- 通过 documentFragment 创建一个
dom
碎片,在它上面批量操作dom
,操作完成之后,再添加到文档中,这样只会触发一次重排。
3. 复制节点
- 复制节点【cloneNode(deep)】,在副本上工作,然后替换【replaceChild(newNode,oldNode)】它!
4. 脱离文档流
- 使用绝对定位会使的该元素单独成为渲染树中
body
的一个子元素,重排开销比较小,不会对其它节点造成太多影响。当你在这些节点上放置这个元素时,一些其它在这个区域内的节点可能需要重绘,但是不需要重排。
5. 优化动画
- 可以把复杂动画效果应用到
position
属性为absolute
或fixed
的元素上,这样对其他元素影响较小。 - 动画效果还应牺牲一些平滑,来换取渲染速度:比如实现一个动画,以1个像素为单位移动这样最平滑,但是Layout就会过于频繁,大量消耗CPU资源,如果以3个像素为单位移动则会好很多