浏览器工作流程

1.构建对象模型

浏览器渲染页面前需要先构建对象模型,根据HTML和CSS输入构建了DOM树和CSSOM树,两树合并成渲染树,然后用于计算每个可见元素的布局,并输出给绘制流程,将像素渲染到屏幕上。

  • 字节 → 字符 → 令牌 → 节点 → 对象模型。(令牌:字符转W3C标准的标签)
  • HTML 标记转换成文档对象模型 (DOM)。
  • CSS 标记转换成 CSS 对象模型 (CSSOM)。
  • DOM 和 CSSOM 是独立的数据结构。(JavaScript可以通过DOM API和CSSOM API来操作两个树)
  • Chrome DevTools Timeline 可以捕获 DOM 和 CSSOM 的构建和处理开销。

0c389301ba794f3ca7b491572d73971d.png

注:建立CSS树是需要比照着DOM树,CSS匹配DOM树从右到左解析CSS的Selector。因为这个过程有性能问题,所以很多人说,DOM树要小,CSS尽量用id和class,千万不要过渡层叠下去等。
Render Tree会把一些不可见的结点去除掉。

2.构建渲染树、布局及绘制

  1. 计算CSS样式
  2. 构建渲染树(Render Tree)
  3. 布局定位坐标和大小,是否换行,各种position, overflow, z-index属性等(Layout)
  4. 最后一步是绘制,最终渲染树将像素渲染到屏幕上。(Reflow,Repaint)

Repaint(重绘): 屏幕的一部分要重画,如某个CSS的背景色变了,但是元素的几何尺寸没有变。

Reflow(重排): 渲染树的一部分或全部发生了变化,元件的几何尺寸变了,我们需要重新验证并计算渲染树。

HTML使用的流式布局,重排会从这个root frame开始递归往下,依次计算所有的结点几何尺寸和位置,在重排过程中,可能会增加一些frame,比如一个文本字符串必需被包装起来。

注:Reflow的成本比Repaint的成本高得多。DOM树里的每个结点都会有reflow方法,一个结点的reflow很有可能导致子结点,甚至父点以及同级结点的reflow。

所以,下面这些动作有很大可能会是成本比较高的。

  • 当你增加、删除、修改DOM结点时,会导致Reflow或Repaint
  • 当你移动DOM的位置,或是搞个动画的时候。
  • 当你修改CSS样式的时候。
  • 当你Resize窗口的时候(移动端没有这个问题),或是滚动的时候。
  • 当你修改网页的默认字体时。

注:display:none会触发reflow,而visibility:hidden只会触发repaint,因为没有发现位置变化。(前者完全看不见,后者会占着布局)

如果在滚屏的时候,我们的页面上的所有的像素都会跟着滚动,那么性能上没什么问题,因为我们的显卡对于这种把全屏像素往上往下移的算法是很快。但是如果你有一个fixed的背景图,或是有些Element不跟着滚动,有些Elment是动画,那么这个滚动的动作对于浏览器来说会是相当相当痛苦的一个过程。你可以看到很多这样的网页在滚动的时候性能有多差。因为滚屏也有可能会造成reflow。

基本上来说,reflow有如下的几个原因:

  • Initial。网页初始化的时候。
  • Incremental。一些Javascript在操作DOM Tree时。
  • Resize。某些元件的尺寸变了。
  • StyleChange。如果CSS的属性发生变化了。
  • Dirty。几个Incremental的reflow发生在同一个frame的子树上。
1
2
3
4
5
6
7
8
var bstyle = document.body.style; // cache
bstyle.padding = "20px"; // reflow, repaint
bstyle.border = "10px solid red"; // 再一次的 reflow 和 repaint
bstyle.color = "blue"; // repaint
bstyle.backgroundColor = "#fad"; // repaint
bstyle.fontSize = "2em"; // reflow, repaint
// new DOM element - reflow, repaint
document.body.appendChild(document.createTextNode('dude!'));

当然,我们的浏览器是聪明的,它不会像上面那样,你每改一次样式,它就reflow或repaint一次。一般来说,浏览器会把这样的操作积攒一批,然后做一次reflow,这又叫异步reflow或增量异步reflow。但是有些情况浏览器是不会这么做的,比如:resize窗口,改变了页面默认的字体,等。对于这些操作,浏览器会马上进行reflow。

但是有些时候,我们的脚本会阻止浏览器这么干,比如:如果我们请求下面的一些DOM值:

  1. offsetTop, offsetLeft, offsetWidth, offsetHeight
  2. scrollTop/Left/Width/Height
  3. clientTop/Left/Width/Height
  4. IE中的 getComputedStyle(), 或 currentStyle

因为,如果我们的程序需要这些值,那么浏览器需要返回最新的值,而这样也会做出一些样式的改变,从而造成频繁的reflow/repaint。

3.优化关键渲染路径与性能

减少reflow/repaint来优化性能:

1.不要一条一条地修改DOM的样式。可以预先定义好class,然后修改DOM的className。
2.把DOM离线后修改

  • 使用documentFragment 对象在内存里操作DOM
  • 先把DOM给display:none(有一次reflow),然后你想怎么改就怎么改。比如修改100次,然后再把他显示出来。
  • clone一个DOM结点到内存里,改完后和在线的那个的交换一下。

3.不要把DOM结点的属性值放在一个循环里当成循环里的变量。会导致大量地读写这个结点的属性。
4.尽可能的修改层级比较低的DOM。当然,改变层级比较底的DOM有可能会造成大面积的reflow,但是也可能影响范围很小。
5.为动画的HTML元件使用fixed或absoult的position,那么修改他们的CSS是不会reflow的。
6.千万不要使用table布局。因为可能很小的一个小改动会造成整个table的重新布局。

最后简述浏览器工作步骤:

  1. 处理 HTML 标记并构建 DOM 树。
  2. 处理 CSS 标记并构建 CSSOM 树。
  3. 将 DOM 与 CSSOM 合并成一个渲染树。
  4. 根据渲染树来布局,以计算每个节点的几何信息。
  5. 将各个节点绘制到屏幕上。
  6. 我们的演示网页看起来可能很简单,实际上却需要完成相当多的工作。如果 DOM 或 CSSOM 被修改,您只能再执行一遍以上所有步骤,以确定哪些像素需要在屏幕上进行重新渲染。

Reference:
https://developers.google.com/speed/docs/insights/rules?csw=1
https://developer.yahoo.com/performance/rules.html?guccounter=1
http://stevesouders.com/hpws/rules.php
https://developers.google.com/web/fundamentals/performance/critical-rendering-path/render-tree-construction?hl=zh-cn
https://www.html5rocks.com/en/tutorials/internals/howbrowserswork/
https://coolshell.cn/articles/9666.html
https://juejin.im/entry/59e1d31f51882578c3411c77
https://www.jianshu.com/p/d616d887953a
https://zhuanlan.zhihu.com/p/33149162