页面优化 ( 重绘 repaint 和回流 reflow )

363 阅读5分钟

重绘和回流是什么

字面上理解,重绘,重新绘画,重新上色,较难产生联想的是回流。

我们都知道,一个页面从加载到完成,首先是构建 DOM 树,然后根据 DOM 节点的几何属性形成 render 树(渲染树),当渲染树构建完成,页面就根据 DOM 树开始布局了,渲染树也根据设置的样式对应的渲染这些节点。

在这个过程中,回流和 DOM 树,渲染树有关,重绘与渲染树有关,怎么去理解呢?

比如我们增删 DOM 节点,修改一个元素的宽高,页面布局发生变化,DOM 树结构随之发生变化,那么肯定要重新构建 DOM 树,而 DOM 树与渲染树是紧密相连的,DOM 树构建完,渲染树也会随之对页面进行再次渲染,这个过程就叫做回流。

当你给一个元素更换颜色,这样的行为是不会影响页面布局的,DOM 树不会变化,但颜色变了,渲染树得重新渲染页面,这就是重绘。

回流的代价要远大于重绘,且回流必然会造成重绘,但重绘不一定会造成回流。

题外话

由于 display:none 的元素在页面不需要渲染,渲染树构建不会包括这些节点;但 visibility:hidden 的元素会在渲染树中,因为 display:none 会脱离文档流,visibility:hidden 虽然看不到,但类似与透明度为0,其实还在文档流中,还是有渲染的过程。

尽量避免使用表格布局,当我们不为表格 td 添加固定宽度时,一列的 td 的宽度会以最宽 td 的宽作为渲染标准,假设前几行 td 在渲染时都渲染好了,结果下面某一行的一个 td 特别宽,table 为了统一宽,前几行的 td 会回流重新计算宽度,这是个很耗时的事情。

重绘和回流有什么区别

结合上面的解释,引起 DOM 树结构变化,页面布局变化的行为叫回流,且回流一定会伴随重绘。 只是样式的变化,不会引起 DOM 树结构变化,页面布局变化的行为叫重绘,且重绘不一定会伴随回流。

回流往往伴随布局的变化,代价较大。重绘只是样式的变化,结构不会变化。

怎么减少回流

说了这么多,我们也知道了,回流要重新构建 DOM 树,渲染树也得重新渲染,很麻烦,那么哪些行为会引起回流,怎么去避免呢?

  1. DOM的增删行为

如果要加多个子元素,最好使用 documentfragment

  1. 几何属性的变化

比如元素宽高变了,border 变了,字体大小变了,这种直接会引起页面布局变化的操作也会引起回流。如果要改变多个属性,最好将这些属性定义在一个 class 中,直接修改 class名,这样只用引起一次回流。

  1. 元素位置的变化

修改一个元素的左右 margin,padding 之类的操作,所以在做元素位移的动画,不要更改 margin 之类的属性,使用定位脱离文档流后改变位置会更好。

  1. 获取元素的偏移量属性

例如获取一个元素的 scrollTop、scrollLeft、scrollWidth、offsetTop、offsetLeft、offsetWidth、offsetHeight 之类的属性,浏览器为了保证值的正确性也会回流取得最新的值,所以如果你要多次操作,最初取完做个缓存。

  1. 页面初次渲染

这样的回流无法避免

  1. 浏览器窗口尺寸改变

resize 事件发生也会引起回流。(当调整浏览器窗口大小时会触发此事件).这里就不列举引起重绘的行为了,记住,回流一定会伴随重绘,所以上面的行为都会重绘,除此之外,例如修改背景颜色,字体颜色之类不影响布局的行为都只引发重绘。

页面优化之 DocumentFragment 对象是什么

DocumentFragment 表示一个没有父级文件的最小文档对象。他被当作一个轻量版的 Document 使用,用于存储已排好版的或尚未打理好格式的 XML 片段。最大的区别是因为 DocumentFragment 不是真实 DOM 树的一部分,它的变化不会引起 DOM 树的重新渲染的操作(reflow),且不会导致性能等问题。

当请求把一个 DocumentFragment 节点插入到文档树时,插入的不是 DocumentFragment 自身,而是它的所有子孙节点。

我们可以将 DocumentFragment 作为一个暂时的 DOM 节点存储器,当我们在 DocumentFragment 修改完成时,我们就可以将存储 DOM 节点的 DocumentFragment 一次性加入到 DOM 树,从而减少回流次数,达到性能优化的目的。

DocumentFragment 对象怎么用?

我们可以使用 document.createDocumentFragment() 创建一个 DocumentFragment,每个新建的 DocumentFragment 都会继承所有的 node 方法。而且 DocumentFragment 拥有 nodeValue,nodeName、nodeType 属性。

let fragment = document.createDocumentFragment();
console.log(fragment.nodeValue);   //null
console.log(fragment.nodeName);    //#document-fragment
cosole.log(fragment.nodeType);     //11

使用 DocumentFragment 能解决直接操作 DOM 引发大量回流的问题,比如我们要给 ul 添加五个 li 节点,区别就像这样:

直接操作 DOM,回流五次:

使用 DocumentFragment 一次性添加,回流一次:

假设我们给 ul 加入五万个 li,分别对比下渲染完成的时间:

<body>
    <ul id="list"></ul>
    <ul id="list1"></ul>
</body>

console.time("time");
let list = document.querySelector("#list"),
    fragment = document.createDocumentFragment(),
    n = 50000;
while(n--){
    fragment.appendChild(document.createElemrnt("li"));
};
list.appendChild(fragment);
console.timeEnd("time")

//直接操作DOM添加节点
console.time("time1");
let list1 = document.querySelector("#list1"),
    i=50000;
while(i--){
    list1.appendChile(document.createElement("li"))
};
console.timeEnd("time1")

多刷新几次对比两者的耗时,time 是使用了 DocumentFragment 的耗时,time1 是直接添加 DOM 的耗时

很明显,time 比 time1 耗时要短很多。 再刷新几次试试

很明显,多次刷新反而还会出现使用 DocumentFragment 耗时更久的情况,性能更差?

由于文档片段的优点在 IE 浏览器下并不明显,反而可能成为多此一举。所以,该类型的节点并不常用