欢迎来到浏览器专题。本期介绍有关浏览器渲染的知识,包括浏览器的渲染流程以及一些相关概念。
如果喜欢本文的话不妨点个赞吧,发现有讲解不准确的地方,也烦请各位大佬指正。
浏览器渲染过程
我们知道,网页的编写是由HTML、CSS、JavaScript构成的,通过浏览器的一系列处理以后就会展示出页面。浏览器是如何通过这些代码转化为一个具体的页面的呢?其实这些都要归功于浏览器的渲染进程 ,它的主要任务就是将我们从服务器上拿到的静态资源渲染为一个可视化界面。
我们可以将它的渲染过程分为五步:
DOM树的构建
浏览器渲染进程使用HTML解析器将HTML代码转换为一个DOM树,其中的每个HTML元素转换为对应的DOM树节点。
为什么要使用DOM树呢?这是因为浏览器不能直接理解HTML代码,同时树状结构作为一种无闭环的数据结构,其具有多层分级的特征,适合用于描述HTML的嵌套结构。
有了这个DOM树,我们的页面也就有了自己的骨架,也就是基本的页面结构已经可以知道了。同时我们的js代码可以访问DOM树,提供了document这个接口用于改变页面的结构、内容等等。
CSSOM树的构建
DOM树可以构建出基本的页面结构,那么CSSOM树就告诉了我们页面的样式。浏览器也无法直接理解CSS代码,它通过CSS解析器解析CSS代码,将CSS代码转换为CSSOM树。 与DOM树描述HTML标签层级不同,CSSOM树描述的层级是选择器的层级关系。
在CSS转换为DOM树之前,浏览器需要先对其进行一系列的预处理。
- 将样式表中的属性值进行标准化处理。我们开发过程中会因为各种原因使用到不同的单位或者写法,CSS解析器需要先将他们统一用于后面的进一步计算。例如:
css
//处理前
div{font-weight:bold}
p{color:blue}
//处理后
div{font-weight:700}
p{color:rgb(0,0,255)}
- 处理样式继承。我们知道,CSS中的某些属性具有继承性,通过在父节点设置该属性,其所有的孩子DOM节点都会具有这个CSS属性。例如:
- font-family
- font-weight
- font-size
- text-align
- color
- ...
- 处理样式层叠。CSS的全称中文为层叠样式表,层叠也是CSS的一个基本特性。层叠就是浏览器对多个样式来源进行叠加,最终确定结果的过程。
同样,CSSOM树可以为JS代码提供接口,用于修改样式。
渲染树构建
有了DOM树和CSSOM树,我们知道了页面的结构和样式,可以构建出渲染树了。渲染树的构建过程就是通过遍历DOM树,同时在CSSOM树中查找到匹配的样式,最终输出一个可以知道每个节点是什么样式的树结构。
这个过程并不都是一对一的,因为我们某些DOM的样式会设置为display:none
样式,那么这个DOM元素实际上是不需要进行渲染的,于是在渲染前我们需要额外构建一颗只包含可见元素的渲染树。
在构建渲染树的过程中,我们可能会在CSSOM树上找到多个匹配的DOM样式,此时需要运用优先级规则选出最终的样式。
页面布局
我们根据上一步输出的渲染树,计算渲染树上的每个节点的样式,可以得到每个元素所在的位置以及大小。浏览器对渲染树进行遍历,根据树状结构得到嵌套关系,以盒子模型的形式将元素写入文档流。
盒子模型在布局过程中计算出元素的各种位置大小信息,在布局完成后同时将其写回渲染树上,这一步输出了布局渲染树。每一个盒子模型也都携带着这个元素的其他样式信息,为下一步的绘制做准备。
页面绘制
分层
拥有了布局渲染树以后,我们并不能直接根据样式对页面进行绘制,因为渲染主线程会对页面进行分层。分层的好处是当元素变化时,只会对影响该元素所在的当前图层进行重新处理,不会影响其他图层,从而减少性能开销。
那么浏览器是如何进行分层的呢?分层的结果主要会受到堆叠上下文和滚动条的相关样式的影响,(z-index
,opacity
,transform
,position
)再结合内存硬件条件去进行分层。因为分层如果过多会影响内存,如果过少又会导致重绘的性能消耗过大。

控制台Layer可以查看到页面的分层情况
补充:
will-change
属性会被单独设置为一个图层,如果页面渲染卡顿时可以用于性能优化
绘制
绘制阶段会将每个图层分成一个个绘制指令,每条指令包含了这次绘制动作的位置、大小、形状等等信息。这些绘制指令会最终被放入一个绘制列表中。

分块
绘制列表准备好以后,渲染进程的渲染主线程会给合成线程发送commit
信息,将绘制列表交给合成线程。而合成线程也不是直接去将页面展示出来,它会对分层后的页面进行分块。
为什么要分块呢?这是考虑到当页面内容过长过多的时候,我们的浏览器会基于用户体验优先绘制出视口部分的内容,此时不会产生过大的性能开销。合成线程将图层划分为图块,每个图块的大小通常为512 *512或者256 *256。
光栅化
合成线程会将块信息交给GPU进程,GPU进程快速完成光栅化,同时为了用户体验会优先处理靠近视口区域的块,最终得到了一块一块的位图。
画
合成线程拿到每个分层、每个分块的位图以后,生成一段指引信息。指引信息会标识出每个位图应该画到屏幕的哪个位置,以及会处理transform
相关的变形操作。
注意:
transform
的变形操作在整个渲染的最后阶段,在合成线程上处理,与渲染主线程无关,效率比渲染主线程上的操作高。
最后将指引信息交给GPU进程,GPU进程调用硬件,完成最终的屏幕成像。
补充
回流与重绘
回流
MDN文档概念:当浏览器必须重新处理和绘制部分或全部页面时,回流就会发生,例如当一个交互式站点更新后。
还记得我们上面说的布局那一步吗?回流也就是改变了元素的布局,使得渲染主进程必须重新进行布局,当然这种布局也是分为局部和全局的
- 局部回流:对渲染树的某部分或某一个渲染对象进行重新布局
- 全局回流:从html根节点开始对渲染树进行重新布局
重绘
MDN文档概念:重绘是指浏览器重新绘制网页,以显示用户界面变化带来的视觉更新,例如交互式网站更新后的视觉更新。
当我们对元素的样式进行修改,但是又没有改变到其的几何属性(例如大小、布局),就会发生重绘,例如改变了元素的color
,background-color
。
总结
一句话,回流一定会引发重绘,但是重绘不一定会引发回流。
Js代码对于渲染过程的影响
当HTML解析器在解析代码时,如果遇到<sciprt>
标签时,会暂停DOM树的构建,转而执行Js代码。如果Js代码中有对DOM元素样式的修改,而此时CSSOM树又没有构建好,那么浏览器会优先构建CSSOM树,这样会严重地影响DOM树地构建效率。如果在不合适的位置调用Js代码,会严重影响渲染效率。
如果按照以下这个写法,那么js代码是不会执行的,因为此时DOM树上p标签还未构建好。我们在刚开始学习Js的时候会将它写在HTML文件最下方,或者在其最前面加上window.onload
,使页面完全加载后再调用Js代码,这样就可以避开Js对渲染效率的影响。
HTML
<html>
<head>
<link href="style.css" rel="stylesheet">
<title>Hello,World</title>
<script type="text/javascript">
var p = document.getElementsByTagName('p')[0];
p.textContent = '哈哈哈';
</script>
</head>
<body>
<p>你好</p>
</body>
</html>
同时也可以在外部链接上使用async属性,使Js代码在就绪后再开始执行。
js
<script type="text/javascript" src="index.js" async></script>