浏览器渲染页面

浏览器渲染过程

  1. 获取 HTML、css、js文件
  2. 将HTML代码形成DOM Tree,解析样式为 CSSOM Tree
  3. 合并 DOM 和 CSSOM,去除不可见元素以及设置了display:none属性的元素,构建 render tree
  4. 对 render tree 的每个元素,其实也是 DOM 元素,计算其形状和位置,进行布局
  5. 将每个节点元素转化为实际像素绘制在视口上,也称“栅格化

p.s.

不可见元素:<html>、<head>、<meta>、<link>、<style>、<script>等

render tree(渲染树):在 Webkit 中这些对象被称为渲染器或渲染对象,而在 Gecko 中称之为“frame”。在渲染树中,每一段文本字符串都表现为独立的渲染器。每一个渲染对象都包含与之对应的 DOM 对象,或者文本块,还加上计算过的样式。换言之,渲染树是一个文档对象模型的直观展示。

重绘

      当render tree中元素的某些属性需要更新,而这些元素只影响外观、风格,不影响元素在网页中的位置(如:background-color、border-color、visibility等),浏览器就会重新构造样式,也就是重绘

重排

  1. DOM 操作(元素添加,删除,修改,或者元素顺序的改变)
  2. 内容变化,包括表单域内的文本改变
  3. CSS 属性的计算或改变
  4. 添加或删除样式表
  5. 更改“类”的属性
  6. 浏览器窗口的操作(缩放,滚动)
  7. 伪类激活(:hover)(在 IE8 中响应速度等性能问题更为明显)

重绘&重排补充知识

触发重排的操作

1. DOM 元素的几何属性变化

当DOM元素的几何属性变化时,渲染树中的相关节点就会失效,浏览器会根据DOM元素的变化重建构建渲染树中失效的节点。之后,会根据新的渲染树重新绘制这部分页面。而且,当前元素的重排也许会带来相关元素的重排。例如,容器节点的渲染树改变时,会触发子节点的重新计算,也会触发其后续兄弟节点的重排,祖先节点需要重新计算子节点的尺寸也会产生重排。最后,每个元素都将发生重绘。可见,重排一定会引起浏览器的重绘,一个元素的重排通常会带来一系列的反应,甚至触发整个文档的重排和重绘,性能代价是高昂的。

2. DOM 树的结构变化

当DOM树的结构变化+时,例如节点的增减、移动等,也会触发重排。浏览器引擎布局的过程,类似于树的前序遍历,是一个从上到下从左到右的过程。通常在这个过程中,当前元素不会再影响其前面已经遍历过的元素。所以,如果在body最前面插入一个元素,会导致整个文档的重新渲染,而在其后插入一个元素,则不会影响到前面的元素。

3. 获取某些属性

浏览器引擎可能会针对重排做了优化。比如Opera,它会等到有足够数量的变化发生,或者等到一定的时间,或者等一个线程结束,再一起处理,这样就只发生一次重排。但除了渲染树的直接变化,当获取一些属性时,浏览器为取得正确的值也会触发重排。这样就使得浏览器的优化失效了。这些属性包括:offsetTop、offsetLeft、 offsetWidth、offsetHeight、scrollTop、scrollLeft、scrollWidth、scrollHeight、clientTop、clientLeft、clientWidth、clientHeight、getComputedStyle() (currentStyle in IE)。所以,在多次使用这些值时应进行缓存。

4. 其他操作(如:调整浏览器窗口大小)

优化实践

改变样式

改变样式的优化方案,就是将多次改变属性的操作合并成一次,如:

// JS:
var changeDiv = document.getElementById(‘changeDiv’);
changeDiv.style.color = ‘#093′;
changeDiv.style.background = ‘#eee';
changeDiv.style.height = ‘200px';

  • 结合 css 的 class 选择器样式:

    // 可以合并为:
    // CSS:
    div.changeDiv {
    background: #eee;
    color: #093;
    height: 200px;
    }
    // JS:
    document.getElementById(‘changeDiv’).className = ‘changeDiv';
  • 直接使用 cssText 属性:

    document.getElementById(‘changeDiv’).cssText = 'background: #eee; color: #093; height: 200px;';

批量修改 DOM

需要对 DOM 进行一系列操作,比如增加删除子节点之类的操作,可以通过下列步骤减少重绘和重排,优化性能:

  1. 使元素脱离文档流
  2. 对其应用多重改变
  3. 把元素带回文档中

这样,整个过程只需要两次重排,中间怎么操作都不会触发。

那么就有第一个问题了:怎么让 DOM 脱离文档?

有三种基本方法:

  • 隐藏元素,应用修改,重新显示
    这个方法原理是利用渲染树的节点不包括隐藏元素,因此,通过改变 display 属性,临时从文档中移除指定元素

  • 使用文档片段,在 DOM 之外构建一个子树,再把它拷贝回文档
    文档片段通过 document.createDocumenyFragment() 创建。它是个轻量级的 document 对象,用来更新和移动节点。当你附加一个片段到文档节点时,实际上被添加进文档的是片段的子节点,而不是片段本身。

  • 将原始元素拷贝到一个脱离文档的节点中,修改副本,完成后再替换原始元素
    直接上例子:

    var old = document.getElementById('test');
    var clone = old.cloneNode(true);
    foo(clone, data); // 更新指定节点数据的通用函数
    old.parentNode.replaceChild(clone, old);

推荐尽可能地使用文档片段,因为它们产生的 DOM 遍历和重排次数最少。

缓存布局信息

浏览器尝试通过队列化修改和批量执行的方式最小化重排次数。当查询布局信息时,比如获取偏移量(offsets)、滚动位置(scroll values)或计算出的样式值时,浏览器为了返回最新值,会刷新队列并应用所有变更。
所以,最好的做法是尽量减少布局信息的获取次数,获取后把它赋值给局部变量,然后再操作局部变量
例子:
把 myElement 元素沿对角线移动,每次移动一个像素,从 (100, 100) 到 (500, 500)。
低效的实现方法:

myElement.style.left = 1 + myElement.style.offsetLeft + 'px';
myElement.style.top = 1 + myElement.style.offsetTop + 'px';
if (myElement.offsetLeft >= 500) {
stopAnimation();
}

这个方法之所以低效,是在于元素每次移动时都会去查询元素的偏移量,导致浏览器需要刷新渲染队列而不利于优化。
好的做法是,获取一次起始位置的值,然后将其赋值给一个变量,然后在动画循环中,直接使用该变量而不再查询偏移量:

var current = myElement.offsetLeft; // 坐标x,y值相同,只需要用一个变量即可
current++;
myElement.style.left = current + 'px';
myElement.style.top = current + 'px';
if (myElement.offsetLeft >= 500) {
stopAnimation();
}

让元素脱离动画流

手风琴导航示例
用图片这种类似手风琴效果的交互模式为例。它通常包括展开区域的集合动画,并将页面其他部分推向下方。而在这个过程中,会导致一次代价昂贵的大规模重排(@: 是一次还是多次?),使得页面感觉卡顿,渲染树中需要重新计算的节点越多,情况越糟糕。

使用下列步骤可以避免页面的大部分重排:

  1. 使用绝对位置定位页面上的动画元素,将其脱离文档流。
  2. 让元素动起来。当它扩大时,会临时覆盖部分页面。但这只是页面一个小区域的重绘过程,不会产生重排并重绘页面的大部分内容。
  3. 当动画结束时恢复定位,从而只会下移一次文档的其他元素

参考: