浏览器渲染机制基础

1字数 3423阅读 2415
  1. 浏览器指的是Chrome系浏览器【Firefox大同小异,IE未知】
  2. 以下提到的“节点”、“标签”和“元素”不做区分,你懂就好

以前,前端面试里时常会被问及或问别人:为什么样式要放在head里,而script[src]标签放在最底部?面试者往往会回答:JS会阻塞页面,CSS不会。

撇开面试的场景,我们可以再问一下自己,为什么这么放呢?阻塞页面指的是什么阻塞,不会阻塞又指的是什么呢,甚至,CSS真的不阻塞吗?

进入CSS3的年代后,前端面试又多这样的题:如何提升transform动画的性能?面试者也往往会回答:将2D变换属性换成3D变换属性,如将translate换成translate3d。

再撇开面试的场景,我们也再问一下自己,为什么这么做呢?也许你会说,使用3D可以告诉浏览器开启GPU渲染,提升渲染性能。是的,那又为什么开启了GPU渲染就可以提升性能呢?

要回答这些问题,单纯从“别人告诉我的”是很难解释清楚的,我们必须从浏览器自身去找寻答案。这就要从浏览器的渲染机制说起了……

两类问题两个阶段,前者要从浏览器第一次渲染页面讲起,后者要从页面变更后浏览器的渲染讲起。

关键渲染路径

浏览器从接收到页面开始到页面显示,这整个过程中的所有步骤,称为关键渲染路径。用户看到页面实际上可以分为两个阶段:页面内容加载完成和页面资源完成,分别对应于DOMContentLoaded和Load。从DevTool-Network面板上看,如下图:

DOMContentLoaded和Load

TL;DR
整个关键渲染路径包括以下几个步骤:

  1. 解析HTML,生成DOM树(DOM)
  2. 解析CSS,生成CSSOM树(CSSOM)
  3. 将DOM和CSSOM合并,生成渲染树(Render-Tree)
  4. 计算渲染树的布局(Layout)
  5. 将布局渲染到屏幕上(Paint)

从上面看出,我们并没有提到脚本JS的处理,并不是脚本处理不在关键渲染路径中,而是因为JS的处理会对1、2产生影响,我们需要单独去解释。

DOM生成

浏览器在获取HTML后,解析HTML代码,将HTML的元素关系转换成一个数据结构,就是我们所熟知的DOM(Document Object Model)。

在解析HTML过程中,会碰到几类特殊的节点需要特殊的处理:

  1. style、link元素以及具有内联样式的元素:交给“CSSOM生成”
  2. script(无论是否外链)元素:见“Script标签的处理”

P.S. 思考:碰到img、video、audio等资源性标签怎么办?

解析完HTML,单纯使用DOM,浏览器并不知道如何渲染这棵树,DOM只是存储了元素的关系,并没有任何渲染信息,如宽高、颜色、背景、定位等。存储这些信息,就需要CSSOM了。扒一张Chrome内部文章的例子来总结:

<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link href="style.css" rel="stylesheet">
    <title>Critical Path</title>
  </head>
  <body>
    <p>Hello <span>web performance</span> students!</p>
    <div>![](awesome-photo.jpg)</div>
  </body>
</html>
DOM生成

CSSOM生成

上面简单提到了,在HTML的解析过程中,会碰到style、link和内联样式,这时,浏览器会将解析DOM换成解析CSSOM,CSSOM和DOM是两个独立的数据结构。

style和内联样式

对这两类样式,浏览器会直接根据样式声明生成CSSOM,因为它们本身就直接含有样式内容。

link

对外联样式,浏览器会首先发送请求,待请求成功,获取外联样式后,浏览器便会解析该外联样式,并生成相应的CSSOM。

由于CSSOM负责存储渲染信息,浏览器就必须保证在合成渲染树之前,CSSOM是完备的,这种完备是指所有的CSS(内联、内部和外部)都已经下载完,并解析完,只有CSSOM和DOM的解析完全结束,浏览器才会进入下一步的渲染,这就是传说中的CSS阻塞渲染。

CSS阻塞渲染意味着,在CSSOM完备前,页面将一直处理白屏状态,这就是为什么样式放在head中,仅仅是为了更快的解析CSS,保证更快的首次渲染。

需要注意的是,即便你没有给页面任何的样式声明,CSSOM依然会生成,默认生成的CSSOM自带浏览器默认样式(default styles)。

样式解析生成的CSSOM便含有渲染信息,这些信息会与DOM一起,生成渲染树Render-Tree。最后,一样附上Chrome官方的事例来个总结:

body { font-size: 16px }
p { font-weight: bold }
span { color: red }
p span { display: none }
img { float: right }
CSSOM生成

在讲渲染树前,我们还需要讲讲一直被我们搁置的script。

Script标签的处理

JS可以操作DOM来修改DOM结构,可以操作CSSOM来修改节点样式,这就导致了浏览器在解析HTML时,一旦碰到script,就会立即停止HTML的解析(而CSS不会),执行JS,再返还控制权。

事实上,JS执行前不仅仅是停止了HTML的解析,它还必须等待CSS的解析完成。当浏览器碰到script元素时,发现该元素前面的CSS还未解析完,就会等待CSS解析完成,再去执行JS。

JS阻塞了HTML的解析,也阻塞了其后的CSS解析,整个解析进程必须等待JS的执行完成才能够继续,这就是所谓的JS阻塞页面。一个script标签,推迟了DOM的生成、CSSOM的生成以及之后的所有渲染过程,从性能角度上讲,将script放在页面底部,也就合情合理了。

渲染树

当DOM和CSSOM构建完成,它们一个存储了节点信息,一个存储了节点渲染信息,都不能直接用来渲染,为此浏览器会将两者结合,生成渲染树(Render-Tree),这棵树就包含了页面所有可见元素及其渲染信息。仍以上述同样的例子:

渲染树

生成渲染树,浏览器做了这些工作:

  1. 从DOM的根节点开始,遍历每个可视节点:script、link、meta都属于不可视节点,另外,display: none的节点也属于不可视节点
  2. 从CSSOM中搜索可视节点的样式
  3. 计算这些样式,将计算值应用到可视节点上

渲染树生成后,还是没有办法渲染到屏幕上,渲染到屏幕需要得到各个节点的位置信息,这就需要布局(Layout)的处理了。

布局

渲染树生成后,浏览器便可以根据渲染树中的样式信息,结合设备的屏幕信息,计算每个元素的位置和尺寸。

渲染

得到了渲染树及其节点的布局信息,浏览器便可以将最终的页面渲染到屏幕。

整个关键渲染路径主要就包括了以上这些步骤,每个步骤的快慢都决定着页面的性能,或者说网站的性能,因此,谈到首屏或者首渲的性能优化,就不得不从关键渲染路径着手,每一步都是有或多或少的可优化点。一些优化建议什么的,就不在本文范围了。

当我们的页面首渲完成后,会有很多页面交互,例如:动画、用户点击、滚屏。所有的交互都会引发浏览器新的渲染操作,这些操作直接影响着用户交互性能,Chrome官网里直接称作渲染性能。

渲染流程

对于渲染,我们首先需要了解一个概念:设备刷新率。

设备刷新率是设备屏幕渲染的频率,通俗一点就是,把屏幕当作墙,设备刷新率就是多久重新粉刷一次墙面。基本我们平常接触的设备,如手机、电脑,它们的默认刷新频率都是60FPS,也就是屏幕在1s内渲染60次,约16.7ms渲染一次屏幕。

这就意味着,我们的浏览器最佳的渲染性能就是所有的操作在一帧16.7ms内完成,能否做到一帧内完成直接决定着渲染性,影响用户交互,这就要求我们需要了解浏览器的一个渲染过程,包括了哪些操作。

完整的渲染流程由以下几步组成:

完整的渲染流程
  • JS:渲染引擎会等待所有的JS操作完成,收集JS对DOM和CSSOM的操作结果
  • Style:样式计算,计算交互引起的样式变更,并应用到相应的节点上
  • Layout:布局,根据新的Style,计算出新的节点位置和尺寸信息
  • Paint:渲染,计算最终的渲染信息(与上述的关键渲染路径-渲染好像不同,其实是一样的,只是上面直接跳到了渲染到屏幕这一步),在实际的渲染中,浏览器会尽可能地在多个层上去渲染,这个层类似PS里的图层概念
  • Composite:合成,将每个渲染层合并,生成最终的一层渲染画面。Paint阶段,每个层独立渲染,并不关心与其他层之间的关系,Composite就需要将这些层以正确的关系合成,有点像PS的导出PNG。合成发生在GPU上

完整的渲染流程就这样,但是,并不是所有的交互都要走一遍这个流程,事实上,从性能角度讲,我们更希望的是每个交互都能省它几个步骤。确实,也是做得到的,不管你是无意识还是有意识,某些交互可以省这么一两个步骤,除了这种走遍天下的渲染流程,“缺胳膊少腿”的渲染流程有以下两种:

  1. 缺Layout
缺少Layout的渲染流程

Layout是计算节点的布局信息——位置和尺寸,当我们修改的样式里不涉及布局,浏览器就会省略这个步骤。例如:color。通常来讲,Layout是很耗渲染性能的,从性能优化角度讲,能避免Layout就避免。除了修改布局属性会触发Layout外,很多获取布局信息的JS操作也会引发Layout,如offsetHeight,getComputedStyle。

  1. 缺Layout和Paint
缺少Layout和Paint的渲染流程

缺Layout我们知道,只要不触发布局属性修改及获取布局信息就可以避免。而避免Paint呢,就是让浏览器将渲染直接交给合成,目前transform和opacity两类样式属性是可以直接跳过Paint的。至于将translate2D变3D,实际上是触发了层提升,使得相应的元素渲染可以在独立层上与其他层并行处理,间接提升了渲染性能。至于别人说的触发GPU加速,也只是因为被新建了一个层。

似乎提升层来提升性能是个很不错的玩法,但是,你的硬件不是无限的,每多一个层,就会多一份内存,因此,控制层数,也是很重要的性能提升。
除了将2D变3D可以达到层的提升,现代浏览器也加上了一个新的样式属性,来“预先”告知浏览器,提升层来处理相应元素的渲染,这个属性名字也是很不错的:will-change

使用该属性,你不必translate3d,只需要:

.my-class {
  will-change: transform;
}

当然,兼容性是个问题,自行caniuse。

正因为transform和opacity可以跳过Paint,并且可以在某种形式下告知浏览器优先以GPU来渲染,才有了现代CSS动画推崇优先使用transform,避免使用position、height等属性的变更来处理动画。一些流行的动画库,如iScroll、Swiper.js等,都是使用transform来处理位置偏移,而非top、left等,就是因为性能更高。

OK,渲染机制就是这么个事,怎么做性能优化,就要根据不同的渲染步骤,配相应的策略,还是那句话,怎么做性能优化,不是本文的目的。

问题

  1. HTML解析时,碰到img、video、audio等资源性标签怎么办?
  2. CSS和JS的请求是在什么时候?
  3. CSS真的不阻塞HTML解析吗?

推荐阅读更多精彩内容