[前端] 重排和重绘
网页的生成过程
- 解析HTML,生成DOM树。
- 解析CSS,生成CSSOM树。
- 结合DOM树和CSSOM树,为每一个节点计算CSS属性,生成渲染树,RenderTree。
- 生成布局(Flow),计算渲染树上所有节点的位置。
- 将布局绘制(Paint)到屏幕上。
- 布局生成和绘制的过程就是渲染。
- 网页生成的时候至少渲染一次。
- 用户交互可能导致重新渲染。
- 渲染是耗时的,应减少不必要的重新渲染以提高网页性能。
重排和重绘的概念
- 重新生成布局,就叫重排(Reflow),也叫回流。
- 重新绘制,就是重绘(Repaint)。
由于布局生成和绘制存在先后顺序关系,重排必定导致重绘,但重绘不一定需要重排。
重排 Reflow
重排与布局有关,当布局发生变化时,也就是元素的几何信息(DOM节点的尺寸和位置)发生变化时,将会触发重排,重新计算元素的几何位置,然后重新绘制。
常见引起重排的属性和方法
- 添加或删除可见的DOM元素;
- 元素尺寸改变——边距、填充、边框、宽度高度;
- 内容变化,比如input框中输入文字;
- 浏览器窗口尺寸改变——resize事件发生时;
- 读取offsetWidth、offsetHeight等属性(浏览器为确保数据准确性,会先强制重排);
- 设置style属性的值(改变了元素的尺寸或位置);
重排影响范围
-
全局范围重排:从根节点
html
开始对整个渲染树进行重排;当一个DOM节点变小,它后续的元素可能位置也发生变化,而它的父元素如果没有固定宽高也可能发生收缩,因此:
一个节点的重排可能影响到它的相邻节点、父节点......从而引发全局范围的重排。
-
局部范围重排:对渲染树的某部分或某一个渲染对象进行重排。
将一个DOM的几何信息固定,当其内部的节点发生重排,则不会影响到外部的节点,是局部范围的重排。
重排总结
- 重排的性能开销与渲染树上需要重新构建的节点数有关。
- 尽量以局部布局的形式组织
html
,将重排控制在局部范围内。
重绘 Repaint
一个元素的外观发生改变,但不改变布局,不影响周围的元素,只是重新绘制该元素的外观,这个过程叫做重绘。
常见引起重绘的属性和方法
color
border-style
background
visibility
border-radius
box-shadow
outline
text-decoration
渲染队列机制
当元素的几何属性被修改,不会立刻导致重排。这个操作会被放到渲染队列,等到队列中的操作到达一定数量或者达到一定的时间间隔,浏览器才会批量执行这些操作。
如下的代码执行了四次对于元素的几何属性的修改,但是只会触发一次重排。
div.style.left = '10px';
div.style.top = '10px';
div.style.width = '20px';
div.style.height = '20px';
强制刷新队列
当访问与布局相关的属性时,浏览器为确保其准确性、准时性,会强制立即执行渲染队列中的任务,即立即重排和重绘。
如下这段代码会触发4次重排和重绘:
div.style.left = '10px';
console.log(div.offsetLeft);
div.style.top = '10px';
console.log(div.offsetTop);
div.style.width = '20px';
console.log(div.offsetWidth);
div.style.height = '20px';
console.log(div.offsetHeight);
强制刷新队列的属性和方法
- offsetTop, offsetLeft, offsetWidth, offsetHeight
- scrollTop, scrollLeft, scrollWidth, scrollHeight
- clientTop, clientLeft, clientWidth, clientHeight
- getComputedStyle(), 或者 IE的 currentStyle
在开发过程中应该尽可能少地访问这些属性,避免多次重排。
重排优化策略
1. 分离读写操作
div.style.left = '10px';
div.style.top = '10px';
div.style.width = '20px';
div.style.height = '20px';
console.log(div.offsetLeft);
console.log(div.offsetTop);
console.log(div.offsetWidth);
console.log(div.offsetHeight);
统一读,再统一写,而不是边读边写。这样可以将重排次数降低到1次。第一个console.log
将渲染队列清空,触发一次重排,而后续的console.log
由于渲染队列本身是空的因此不会触发重排。
2. 样式集中改变
不要分批次的修改样式属性,而是一次性修改。
建议提前写好若干个CSS的class
选择器,然后 JS 负责切换。
// bad
box.style.left = '10px';
box.style.top = '200px';
box.style.transform = 'scale(1.1)';
// good
box.classList.add('className1');
box.classList.remove('className2');
3. 缓存布局信息
布局信息的每一次读取都会导致强制重排,如果确保布局信息在当前情景下不会有变动,可以使用一个临时变量缓存使用。
// bad 强制刷新 触发两次重排
div.style.left = div.offsetLeft + 1 + 'px';
div.style.top = div.offsetTop + 1 + 'px';
// good
var curLeft = div.offsetLeft;
var curTop = div.offsetTop;
div.style.left = curLeft + 1 + 'px';
div.style.top = curTop + 1 + 'px';
4. 离线修改DOM
DOM设置display: none;
,将其从渲染树中移除,然后再进行复杂修改操作,最后再将其显示出来。
整个过程包含隐藏和显示共两次重排。
或者使用DocumentFragment
创建一个DOM碎片,然后在其上面批量操作DOM,操作完成之后再添加到文档中,这样只会触发一次重排。
5. 目标元素设置绝对定位
设置position
为absolute
或fixed
,这样就只会影响子节点的重排,不会影响外部。
6. 不要使用table布局
table中的某一个元素触发重排将导致整个table重排。
如果是在维护远古项目不得不使用table布局,可以设置table-layout: fixed;
或者table-layout: auto;
,这样可以让table一行一行的渲染,限制重排的影响范围。