在浏览器从获取到HTML,然后解析,渲染的过程中,到底发生了什么?

页面呈现六部曲

1.解析HTML,构建DOM树。
2.解析CSS,生成CSS规则树。
在chrome的Timeline 工具中对应的阶段是:Re-caculate。为什么是 Re-caculate Style 呢?这是因为浏览器本身有 User Agent StyleSheet,所以最终的样式是我们的样式代码样式与用户代理默认样式覆盖/重新计算得到的。
3.合并 DOM 树与 CSSOM 树为 Render 树
4.布局(Layout)
5.绘制(Paint)
6.复合图层化(Composite)
多个复合层的合成,最终合成的页面被用户看到。

六部曲中存在的阻塞

虽然六部曲看似和谐,分工合作,有序进行。但是实际上这里面却是波云诡谲,风起云涌,就像平时的工作一样,看似你和我各司其职,分工明确,但是实际干起活来却可能因为某一个人的某一环而阻滞整个进度。

那么下面我们来分析一下六部曲中存在的阻塞:

  1. 当遇到 JavaScript 脚本或者外部 JavaScript 代码时,浏览器便停止 DOM 的构建(阻塞1)
    然而,是否停下 DOM 的构建的同时,立马就执行 JavaScript 代码或者下载外部脚本执行,其实还是要视情况而定
  2. 当遇到 <script>标签需要执行脚本代码时,浏览器会检查是否这个 <script>标签以上的 CSS 文件是否已经加载并用于构建了 CSSOM,如果 <script>上部还有 CSS 样式没加载,则浏览器会等待 <script>上方样式的加载完成才会执行该 <script>内的脚本,(阻塞2)
  3. DOM 树与 CSSOM 树的成功构建是后面步骤的根基(同步阻塞)
  4. 同时外部脚本、外部样式表的下载也是耗费时间较多的点

六部曲之——DOM树的构建

浏览器构建DOM树可以简单的总结为以下几步:

  1. 转码(Bytes -> Characters)—— 读取接收到的 HTML 二进制数据,按指定编码格式将字节转换为 HTML 字符串
  2. Tokens 化(Characters -> Tokens)—— 解析 HTML,将 HTML 字符串转换为结构清晰的 Tokens,每个 Token 都有特殊的含义同时有自己的一套规则
  3. 构建 Nodes(Tokens -> Nodes)—— 每个 Node 都添加特定的属性(或属性访问器),通过指针能够确定 Node 的父、子、兄弟关系和所属 treeScope(例如:iframe 的 treeScope 与外层页面的 treeScope 不同)
  4. 构建 DOM 树(Nodes -> DOM Tree)—— 最重要的工作是建立起每个结点的父子兄弟关系
    1
    在 Chrome 开发者工具下 Timeline 面板的 Parse HTML 阶段对应着 DOM 树的构建。

扩展阅读:从Chrome源码看浏览器如何构建DOM树
留意这篇文章的这些点:

  1. DOM 构建时对 DOCType 处理
  2. DOCType 的不同或漏缺带来的文档解析模式(怪异模式、有限怪异模式、标准模式)的影响
  3. 处理开标签与闭标签的压栈、弹栈处理
  4. Chromium 对待自定义标签的处理
  5. JavaScript 方法查找 DOM 的过程,使用 ID、类名、复杂选择器查找 DOM 的对比

六部曲之——CSSOM树的构建

CSSOM 树的构建 “原料” 的来源有:外部 CSS 文件、内部样式、内联样式。

CSSOM 树的构建其实是一个 样式的重新计算 的过程,为什么是重新计算呢?

1
2
用户代理(即浏览器)本身有一套内置样式表,所以我们最终的 CSSOM 树其实是用户代理样式与页面所有样式的重新计算。
所以在 Chrome 浏览器开发者工具的 Timeline 面板下,CSSOM 树的构建对应的是 Recalculate Style 阶段

与 DOM 树的构建过程相似,CSSOM 的构建也要经历以下过程:

最终构建的 CSSOM 树大致如下:

六部曲之——渲染树的构建

1.DOM 树与 CSSOM 树融合成渲染树
2.渲染树只包括渲染页面需要的节点

1
2
排除 <script> <meta> 等功能化、非视觉节点
排除 display: none 的节点

过程大致如下:

六部曲之——布局

Layout 阶段做的工作:确定页面各元素的位置、尺寸。

1
Layout 在 Chrome 开发者工具 Timeline 面板中被归并到 Paint 阶段

当元素某些样式变更/JavaScript 执行某些样式请求,会导致 Layout trashing,又叫做回流(Reflow)。

六部曲之——绘制

一旦布局(Layout)步骤完成,浏览器便触发 Paint SetupPaint 事件(渲染引擎底层概念),执行 paint 操作,结合渲染树与布局信息绘制实际像素。

1
注:在 Timeline 工具内,Layout 与 Paint 两个过程被统一归并到 Paint 阶段。

六部曲之——复合图层化

在很多情况下,我们不会将复合图层化归入页面呈现的必要过程。图层化是浏览器为了充分利用已有渲染成果(缓存渲染成果),最小化 GPU 运算,将“脏区”提升为复合图层,隔离变化影响的操作。

详情见https://segmentfault.com/a/1190000008015671

如何创建layer

1.3D 或透视变换(perspective transform) CSS 属性
2.使用加速视频解码的元素,如<video>
3.拥有 3D (WebGL) 上下文或加速的 2D 上下文的,如<canvas>
4.混合插件(如 Flash)
5.对自己的 opacity 做 CSS 动画或使用一个动画 webkit 变换的元素
6.拥有加速 CSS 过滤器的元素,如CSS filters
7.元素有一个包含复合层的后代节点(换句话说,就是一个元素拥有一个子元素,该子元素在自己的层里)
8.元素有一个 z-index 较低且包含一个复合层的兄弟元素(换句话说就是该元素在复合层上面渲染)
9.在webkit内核的浏览器中,如果有上述情况,则会创建一个独立的layer。

拒绝layer泛滥

当对元素创建layer之后,会节省layout和paint阶段,但是layer不能泛滥。 layer跟psd中图层很像,我们知道在psd中图层越多,psd图片就会越大。 同理,layer越多,占用的内存就越多,当在内存有限的移动设备上(手机),过多的渲染带来的开销超过了它在性能上的改善,得不偿失; 白白的给元素添加layer,一般通过* {-webkit-transform: translateZ(0);}为元素添加layer 但创建layer的原则:当且仅当需要的时候才为元素创建渲染层。

注意事项

渲染分为三阶段:layout,paint,composite layer,修改不同的css属性会触发不同的阶段,触发的阶段越靠前,渲染的代价越高。
1:尽量避免触发layout(位置相关的可通过transform代替 top left),paint。 除写css属性外,读取css的位置大小相关属性会导致触发layout阶段,要分离读写,减少layout。
2:应该尽量避免重绘,并且尽可能的使绘制区域最小,以提升页面性能。 但是有些必须使用的样式效果还是要用的,比如fixed等重要的是作为前端人员,能够预估这些代码所带来的性能损耗及所造成的影响。
3:避免组合触发 比如滚动和hover效果,hover中若使用border-shadow,border-style修改,则会损耗较大的性能,有可能会触发丢帧的现象。 改进办法:在滚动时,增加计时器,可先把hover效果禁掉,滚动结束后再打开hover效果。

浏览器的渲染流程方向的优化手段

知道了页面渲染的原理,那么我们也就得到了页面性能优化的依据。提炼六部曲中每一步的优化空间,针对六部曲中的每一步提出针对性的优化方案也就能达到我们最终的优化目的。

优化不可避免的阻塞:优化关键呈现路径

关键呈现路径里的一些概念:

  • 关键资源:可能阻止网页首次呈现的资源。
  • 关键路径长度:即往返过程数量,或提取所有关键资源所需的总时间。
  • 关键字节:实现网页首次呈现所需的总字节数,是所有关键资源的传输文件大小总和。 带有一个 HTML 网页的首个示例包含一项关键资源(HTML 文档),关键路径长度也与 1 个网络往返过程(假设文件较小)相等,而且总的关键字节数正好是 HTML 文档本身的传输大小。

优化关键呈现路径的指导原则:

  • 尽量减少关键资源数量。
  • 尽量减少关键字节数。
  • 尽量缩短关键路径的长度。

优化关键呈现路径常规步骤:

  • 分析和描述关键路径:资源数量、字节数、长度。
  • 尽量减少关键资源数量:删除相应资源、延迟下载、标记为异步资源等等。
  • 优化剩余关键资源的加载顺序:你需要尽早下载所有关键资源,以缩短关键路径长度。
  • 尽量减少关键字节数,以缩短下载时间(和往返次数)。

优化关键呈现路径的具体建议:

  • 文件合并、压缩

  • 推荐使用异步(async) JavaScript 资源,或使用延迟(defer)执行的 JavaScript

  • 一般 <script>脚本的靠后书写

  • 避免运行时间长的 JavaScript,耗时任务的拆分,chunk 化运行

    1
    例如:使用定时器将大任务拆分为小任务,使得浏览器得到空隙做其他事情。
  • 避免使用 CSS import

  • 内联、内部化阻止呈现的 CSS
    1
    一般不采用,百度、Google 这样的极度重视性能与体验的服务才可能这样做。

针对复合图层化的优化

因为浏览器有图层化这个机制,那么我们就搞懂它并充分利用吧。

复合图层化机制是怎样的呢?

如果某些属性的变更(transform、opacity)满足以下条件:

  • 不影响文档流
  • 不依赖文档流
  • 不会造成重绘

那么,这些属性变更时,就需要一种机制:
能将属性变更的部分与页面其他部分隔离开来,对其他部分已经渲染完的进行缓存,变更的部分在单独的图层上进行,然后对缓存的部分与变更的图层进行合成。
关键字:缓存、隔离、图层合成
使用 transform 与 opacity 进行属性变更是经典的复合图层优化方法,以下是其他会提升元素为复合图层的场景:
1.3d 或透视变换 CSS 属性,例如 translate3d, translateZ 等等(JS 一般通过这种方式,使元素获得复合图层)
2.<video> <iframe> <canvas> <webgl>等元素
3.混合插件(如flash)
4.元素自身的 opacity 和 transform 做 CSS 动画
5.拥有CSS Filter的元素
6.使用 will-change 属性
7.position:fixed
8.元素有一个 z-index 较低且包含一个复合层的兄弟元素(换句话说就是该元素在复合层上方)

图层化的优势

很容易看出来:充分利用缓存、隔离的思想,无需像回流、重绘那么大性能(GPU、CPU)开支,图层化能带来 动画性能的提升

图层化的潜在问题/弊端 —— 内存开销
因为图层化的存在,每个图层对需要在内存中存储该图层相关的信息,当图层太多会造成内存开销过大的情况。

因为开销,所以节制

内存开销在桌面端可能还能接受,但是在资源有限的移动端,复合图层过多便可能导致内存开支过大,页面反而变得停滞、卡顿,甚至浏览器假死,系统无法正常运行。

针对回流的优化

首先,我们需要知道什么情况下会触发浏览器的repaint/reflow:
查询哪些属性会触发Layout/Paing/Composite,可参考:
https://csstriggers.com/

技术使用优先级:

1.CSS3 > JavaScript
2.属性变更优先考虑顺序(性能表现排序)

  • transfrom, opacity
  • background-color等
  • position top、bottom、left、right
  • width、height等
  • margin、padding、border

What forces layout

能够引起重绘和回流的属性修改/查找:

  • Element
    Box metrics
    elem.offsetLeft, elem.offsetTop, elem.offsetWidth, elem.offsetHeight, elem.offsetParent
    elem.clientLeft, elem.clientTop, elem.clientWidth, elem.clientHeight
    elem.getClientRects(), elem.getBoundingClientRect()

Scroll stuff
elem.scrollBy(), elem.scrollTo()
elem.scrollIntoView(), elem.scrollIntoViewIfNeeded()
elem.scrollWidth, elem.scrollHeight
elem.scrollLeft, elem.scrollTop also, setting them

Focus
elem.focus() can trigger a double forced layout (source)

Also…
elem.computedRole, elem.computedName
elem.innerText (source)

  • getComputedStyle

  • ……

详情查看:https://gist.github.com/yangfch3/fc4877b144c8964a9c20efd975de5aa9
JavaScript 存在这样的机制:当连续有大量 DOM 样式的操作时,出于性能考虑,防止零碎变更导致频繁的回流、重绘,会尽可能地将这些操作先缓存起来,然后一次性地变更。这个机制我们难以察觉但是确实存在。
然而当我们进行某些 DOM 样式的读、写时,出于时效性的考虑,则会立即触发浏览器回流、重绘以返回正确、合理的值。

减少渲染时的内存消耗

1.避免浏览器的隐式合成。

  • 保持动画的对象的z-index尽可能的高。理想的,这些元素应该是body元素的直接子元素。当然,这不是总可能的。所以你可以克隆一个元素,把它放在body元素下仅仅是为了做动画。
  • 将元素上设置will-change CSS属性,元素上有了这个属性,浏览器会提升这个元素成为一个复合层(不是总是)。这样动画就可以平滑的开始和结束。但是不要滥用这个属性,否则会大大增加内存消耗。

2.减小复合层的尺寸
看一下两张图片,有什么不同吗?

这两张图片视觉上是一样的,但是它们的尺寸一个是39kb;另外一个是400b。不同之处在于,第二个纯色层是通过scale放大10倍做到的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<div id="a"></div>
<div id="b"></div>

<style>
#a, #b {
will-change: transform;
}

#a {
width: 100px;
height: 100px;
}

#b {
width: 10px;
height: 10px;
transform: scale(10);
}
</style>

对于图片,你要怎么做呢?你可以将图片的尺寸减少5%——10%,然后使用scale将它们放大;用户不会看到什么区别,但是你可以减少大量的存储空间。

减少渲染时的计算数量

1.用css动画而不是js动画
css动画有一个重要的特性,它是完全工作在GPU上。因为你声明了一个动画如何开始和如何结束,浏览器会在动画开始前准备好所有需要的指令;并把它们发送给GPU。
而如果使用js动画,浏览器必须计算每一帧的状态;
为了保证平滑的动画,我们必须在浏览器主线程计算新状态;
把它们发送给GPU至少60次每秒。除了计算和发送数据比css动画要慢,主线程的负载也会影响动画;
当主线程的计算任务过多时,会造成动画的延迟、卡顿。

所以尽可能地使用基于css的动画,不仅仅更快;也不会被大量的js计算所阻塞。

节流函数

惰性载入函数

重任务分片多帧

知道原理之后,常用的有如下几点优化方法:

1.避免在document上直接进行频繁的DOM操作,如果确实需要可以采用off-document的方式进行,具体的方法包括但不完全包括以下几种:
(1). 先将元素从document中删除,完成修改后再把元素放回原来的位置

(2). 将元素的display设置为”none”,完成修改后再把display修改为原来的值

(3). 如果需要创建多个DOM节点,可以使用DocumentFragment创建完后一次性的加入document

2.集中修改样式
(1). 尽可能少的修改元素style上的属性

(2). 尽量通过修改className来修改样式

(3). 通过cssText属性/className来设置样式值

1
document.getElementById("d1").style.cssText = "color:red; font-size:13px;";

3.缓存Layout属性值
对于Layout属性中非引用类型的值(数字型),如果需要多次访问则可以在一次访问时先存储到局部变量中,之后都使用局部变量,这样可以避免每次读取属性时造成浏览器的渲染。

1
var width = el.offsetWidth; var scrollLeft = el.scrollLeft;

4.设置元素的position为absolute或fixed

5.不要使用table布局。

6.css动画中尽量只使用transform和opacity,这不会发生重排和重绘。
如上所说,transform和opacity保证了元素属性的变化不影响文档流、也不受文档流影响;并且不会造成repaint。
有些时候你可能想要改变其他的css属性,作为动画。例如:你可能想使用background属性改变背景:

1
2
3
4
5
6
7
8
9
10
<div class="bg-change"></div>
.bg-change {
width: 100px;
height: 100px;
background: red;
transition: opacity 2s;
}
.bg-change:hover {
background: blue;
}

在这个例子中,在动画的每一步;浏览器都会进行一次重绘。我们可以使用一个复层在这个元素上面,并且仅仅变换opacity属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<div class="bg-change"></div>
<style>
.bg-change {
width: 100px;
height: 100px;
background: red;
}
.bg-change::before {
content: '';
display: block;
width: 100%;
height: 100%;
background: blue;
opacity: 0;
transition: opacity 20s;
}
.bg-change:hover::before {
opacity: 1;
}
</style>

回答几个问题 自测

如何理解 getComputedStyle?

在尚未梳理知识体系前,大概会这样回答:

普通版本: getComputedStyle会获取当前元素所有最终使用的CSS属性值(最终计算后的结果),通过 window.getComputedStyle等价于 document.defaultView.getComputedStyle调用

详细版本: window.getComputedStyle(elem,null).getPropertyValue(“height”)可能的值为 100px,而且,就算是css上写的是 inherit, getComputedStyle也会把它最终计算出来的。不过注意,如果元素的背景色透明,那么 getComputedStyle获取出来的就是透明的这个背景(因为透明本身也是有效的),而不会是父节点的背景。所以它不一定是最终显示的颜色。

就这个API来说,上述的回答已经比较全面了。

但是,其实它是可以继续延伸的。

譬如现在会这样回答:

getComputedStyle会获取当前元素所有最终使用的CSS属性值, window.和 document.defaultView.等价…

getComputedStyle会引起回流,因为它需要获取祖先节点的一些信息进行计算(譬如宽高等),所以用的时候慎用,回流会引起性能问题。然后合适的话会将话题引导回流,重绘,浏览器渲染原理等等。当然也可以列举一些其它会引发回流的操作,如 offsetXXX, scrollXXX, clientXXX, currentStyle等等

visibility:hidden和 display:none的区别?

可以如下回答:

普通回答,一个隐藏,但占据位置,一个隐藏,不占据位置

进一步, display由于隐藏后不占据位置,所以造成了dom树的改变,会引发回流,代价较大

再进一步,当一个页面某个元素经常需要切换 display时如何优化,一般会用复合层优化,或者要求低一点用 absolute让其脱离普通文档流也行。然后可以将话题引到普通文档流, absolute文档流,复合图层的区别,

再进一步可以描述下浏览器渲染原理以及复合图层和普通图层的绘制区别(复合图层单独分配资源,独立绘制,性能提升,但是不能过多,还有隐式合成等等)

上面这些大概就是知识系统化后的回答,会更全面,容易由浅入深,而且一有机会就可以往更底层挖

参考: