关键渲染路径
浏览器从接收到页面开始到页面显示,这整个过程中的所有步骤,称为关键渲染路径。用户看到页面实际上可以分为两个阶段:页面内容加载完成和页面资源完成,分别对应于DOMContentLoaded和Load。从DevTool-Network面板上看,如下图:
DOMContentLoaded和Load
TL;DR
整个关键渲染路径包括以下几个步骤:
- 解析HTML,生成DOM树(DOM)
- 解析CSS,生成CSSOM树(CSSOM)
- 将DOM和CSSOM合并,生成渲染树(Render-Tree)
- 计算渲染树的布局(Layout)
- 将布局渲染到屏幕上(Paint)
从上面看出,我们并没有提到脚本JS的处理,并不是脚本处理不在关键渲染路径中,而是因为JS的处理会对1、2产生影响,我们需要单独去解释。
DOM生成
浏览器在获取HTML后,解析HTML代码,将HTML的元素关系转换成一个数据结构,就是我们所熟知的DOM(Document Object Model)。
在解析HTML过程中,会碰到几类特殊的节点需要特殊的处理:
- style、link元素以及具有内联样式的元素:交给“CSSOM生成”
- script(无论是否外链)元素:见“Script标签的处理”
P.S. 思考:碰到img、video、audio等资源性标签怎么办?
解析完HTML,单纯使用DOM,浏览器并不知道如何渲染这棵树,DOM只是存储了元素的关系,并没有任何渲染信息,如宽高、颜色、背景、定位等。存储这些信息,就需要CSSOM了。扒一张Chrome内部文章的例子来总结:
Critical Path
Hello web performance students!
![](awesome-photo.jpg)
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),这棵树就包含了页面所有可见元素及其渲染信息。仍以上述同样的例子:
渲染树
生成渲染树,浏览器做了这些工作:
- 从DOM的根节点开始,遍历每个可视节点:script、link、meta都属于不可视节点,另外,display: none的节点也属于不可视节点
- 从CSSOM中搜索可视节点的样式
- 计算这些样式,将计算值应用到可视节点上
渲染树生成后,还是没有办法渲染到屏幕上,渲染到屏幕需要得到各个节点的位置信息,这就需要布局(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上
完整的渲染流程就这样,但是,并不是所有的交互都要走一遍这个流程,事实上,从性能角度讲,我们更希望的是每个交互都能省它几个步骤。确实,也是做得到的,不管你是无意识还是有意识,某些交互可以省这么一两个步骤,除了这种走遍天下的渲染流程,“缺胳膊少腿”的渲染流程有以下两种:
- 缺Layout
缺少Layout的渲染流程
Layout是计算节点的布局信息——位置和尺寸,当我们修改的样式里不涉及布局,浏览器就会省略这个步骤。例如:color。通常来讲,Layout是很耗渲染性能的,从性能优化角度讲,能避免Layout就避免。除了修改布局属性会触发Layout外,很多获取布局信息的JS操作也会引发Layout,如offsetHeight,getComputedStyle。
- 缺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,渲染机制就是这么个事,怎么做性能优化,就要根据不同的渲染步骤,配相应的策略,还是那句话,怎么做性能优化,不是本文的目的。