阅读 154

页面渲染之回流与重绘

页面的呈现流程

DOM Tree

浏览器把获取到的HTML代码解析成DOM Tree,HTML中的每个tag都是DOM Tree中的1个节点,根节点就是我们常用的document对象。DOM Tree里包含了所有HTML标签,包括display:none,还有用JS动态添加的元素等。

样式结构体

浏览器把所有样式(用户定义的CSS和用户代理)解析成样式结构体,在解析的过程中会去掉浏览器不能识别的样式,比如IE会去掉-moz开头的样式,而FF会去掉_开头的样式。

render tree

DOM Tree样式结构体组合后构建render tree, render tree类似于DOM tree,但区别很大,render tree能识别样式,render tree中每个node都有自己的style,而且 render tree不包含隐藏的节点 (比如display: none的节点,还有head节点),因为这些节点不会用于呈现,而且不会影响呈现的,所以就不会包含到 render tree中。注意 visibility: hidden隐藏的元素还是会包含到 render tree中的,因为visibility: hidden 会影响布局(layout),会占有空间。根据CSS2的标准,render tree中的每个节点都称为Box (Box dimensions),理解页面元素为一个具有填充、边框、边距和位置的盒子。

浏览器绘制

一旦render tree构建完毕后,浏览器就可以根据render tree来绘制页面了。

总结如下图

image.png

回流与重绘

当render tree中的一部分(或全部)因为元素的尺寸,布局,隐藏等改变而需要重新构建。这就称为回流(reflow)

当render tree中的一些元素需要更新属性,而这些属性只是影响元素的外观,风格,而不会影响布局的,比如background-color。则就叫称为重绘(repaints)

每个页面至少需要一次回流,就是在页面第一次加载的时候。在回流的时候,浏览器会使渲染树中受到影响的部分失效,并重新构造这部分渲染树。完成回流后,浏览器会重新绘制受影响的部分到屏幕中,该过程为重绘。

注意: 回流必将引起重绘,而重绘不一定会引起回流,回流比重绘的代价更高

回流何时发生

当页面布局和几何属性改变时就需要回流。下述情况会发生浏览器回流:

  1. 添加或者删除可见的DOM元素;

  2. 元素位置改变;

  3. 元素尺寸改变——边距、边框、填充、宽度和高度;

  4. 内容改变——比如文本改变或者图片大小改变而引起的计算值宽度和高度改变;

  5. 页面渲染初始化;

  6. 浏览器窗口尺寸改变——resize事件发生时;

const box = document.getElementById("box").style;
box.padding = "2px";   // 回流+重绘
box.border = "1px solid red";  // 再一次 回流+重绘
box.fontSize = "14px";    // 回流+重绘
document.getElementById("box").appendChild(document.createTextNode('abc!')); // 回流+重绘
复制代码

重绘何时发生

元素的属性或者样式发生变化。

const box = document.getElementById("box").style;
box.color = "red";    // 重绘
box.backgroudColor = "blue";    // 重绘
复制代码

优化(减少回流、重绘)

因回流的开销较大,如果每个操作都去回流重绘的话,浏览器可能就会受不了。所以很多浏览器都会优化这些操作。

浏览器会维护1个队列,把所有会引起回流、重绘的操作放入这个队列,等 队列中的操作到了一定的数量或者到了一定的时间间隔,浏览器就会flush 队列,进行一个批处理。这样就会让多次的回流、重绘变成一次回流重绘。

但是有时上面的方法会失效,原因是:

有些情况,当向浏览器请求一些style信息的时候,就会让浏览器强制flush队列,比如:

  1. offsetTop, offsetLeft, offsetWidth, offsetHeight
  2. scrollTop/Left/Width/Height
  3. clientTop/Left/Width/Height
  4. width,height
  5. 请求了getComputedStyle(), 或者 IE的 currentStyle

当你请求上面的一些属性的时候,浏览器为了给你最精确的值,需要flush队列,因为队列中可能会有影响到这些值的操作。即使你获取元素的布局和样式信息跟最近发生或改变的布局信息无关,浏览器都会强行刷新渲染队列。

优化方法举例:

  1. 减少对render tree的操作(将多次改变样式属性的操作合并成一次操作)。

    let box = document.getElementById("box").style;
    // bad
    box.color = "red";    // 重绘
    box.size = "14px";    // 回流、重绘
    // good
    box.bord = '1px solid red'
    复制代码
  2. 在内存中多次操作节点,完成后再添加到文档中去。例如要异步获取表格数据,渲染到页面。可以先取得数据后在内存中构建整个表格的html片段,再一次性添加到文档中去,而不是循环添加每一行。

  3. 由于display属性为none的元素不在渲染树中,对隐藏的元素操作不会引发其他元素的重排。如果要对一个元素进行复杂的操作时,可以先隐藏它,操作完成后再显示。这样只在隐藏和显示时触发2次重排。

  4. 添加css样式,而不是利用js控制样式

  5. 直接改变className,如果动态改变样式,则使用cssText(考虑没有优化的浏览器)

    // bad
    elem.style.left = x + "px";
    elem.style.top = y + "px";
    // good
    elem.style.cssText += ";left: " + x + "px;top: " + y + "px;";
    复制代码
  6. 不要经常访问会引起浏览器flush队列的属性,如果你确实要访问,利用缓存

    // bad
    for (var i = 0; i < len; i++) {
      el.style.left = el.offsetLeft + x + "px";
      el.style.top = el.offsetTop + y + "px";
    }
    // good
    var x = el.offsetLeft,
        y = el.offsetTop;
    for (var i = 0; i < len; i++) {
      x += 10;
      y += 10;
      el.style = x + "px";
      el.style = y + "px";
    }
    复制代码
  7. 将需要多次重排的元素,position属性设为absolute或fixed,这样此元素就脱离了文档流,它的变化不会影响到其他元素,减少回流的Render Tree的规模。例如有动画效果的元素就最好设置为绝对定位;

  8. 避免使用table布局:尽量不要使用表格布局,如果没有定宽表格一列的宽度由最宽的一列决定,那么很可能在最后一行的宽度超出之前的列宽,引起整体回流造成table可能需要多次计算才能确定好其在渲染树中节点的属性,通常要花3倍于同等元素的时间。

  9. 尽可能在DOM树的最末端改变class,尽可能在DOM树的里面改变class(可以限制回流的范围)

  10. requestAnimationFrame:能保证浏览器在正确的时间进行渲染。

  11. 保持 DOM 操作“原子性”

// bad
var newWidth = ele.offsetWidth + 10
ele.style.width = newWidth + 'px'

var newHeight = ele.offsetHeight + 10
ele.style.height = newHeight + 'px'

// good 读写分离,批量操作
var newWidth = ele.offsetWidth + 10 // read
var newHeight = ele.offsetHeight + 10 // read
ele.style.width = newWidth + 'px' // write
ele.style.height = newHeight + 'px' // write
复制代码
  1. 使用 classList 代替 className

className 只要赋值,就一定出现一次 rendering 计算;classList 的 add 和 remove,浏览器会进行样式名是否存在的判断,以减少重复的 rendering。

ele.className += 'something'
ele.classList.add('something')
ele.classList.remove('something')
复制代码
  1. 对元素进行“离线操作”,完成后再一起更新:
    1. 使用 DocumentFragment 进行缓存操作,引发一次回流和重绘 了解DocumentFragment 给我们带来的性能优化
    2. 元素操作前使用 display: none,完成后再将其显示出来,这样只会触发一次回流和重绘。
    3. 使用 cloneNode + replaceChild 技术,引发一次回流和重绘。

假如需要在下面的 html 中添加两个 li 节点:

<ul id=""> </ul>
复制代码

使用 JavaScript:

let ul = document.getElementByTagName('ul') 
let man = document.createElement('li') 
man.innerHTML = 'man' 
ul.appendChild(li)   
let woman = document.createElement('li') 
woman.innerHTML = 'woman' ul.appendChild(woman)
复制代码

上述代码会发生两次回流,假如使用 display: none 的方案,虽然能够减少回流次数,但是会发生一次闪烁,这时候使用 DocumentFragment 的优势就体现出来了。

DocumentFragment 有两大特点:

  1. DocumentFragment 节点不属于文档树,继承的 parentNode 属性总是 null。
  2. 当请求把一个 DocumentFragment 节点插入文档树时,插入的不是 DocumentFragment 自身,而是它的所有子孙节点。这使得 DocumentFragment 成了有用的占位符,暂时存放那些一次插入文档的节点。它还有利于实现文档的剪切、复制和粘贴操作。
let fragment = document.createDocumentFragment()

let man = document.createElement('li')
let woman = document.createElement('li')
man.innerHTML = 'man'
woman.innerHTML = 'woman'
fragment.appendChild(man)
fragment.appendChild(woman)

document.body.appendChild(spanNode)
复制代码

可见 DocumentFragment 是一个孤儿节点,没爹就能出生,但是在需要它的时候,它又无私地把孩子奉献给文档树,然后自己默默离开。

文章分类
前端
文章标签