前言
先抛出几个问题:
- CSS解析会阻塞HTML渲染吗?
- JS解析会阻塞HTML渲染吗?
- 什么是reflow?
- 什么是repaint?
- 为什么transiform效率极高?
有一个很经典的面试题:从输入URL到页面渲染发生了什么?在这短短的1s不到的时间,浏览器完成了及其复杂的处理,很多处理方式体现了世界上顶流工程师的思维。这篇文章的目的是了解、学习和借鉴他们的思维和处理问题的方式,下面,让我们开始吧!
一、浏览器渲染原理
从图上可以很清晰的看到代码经过一系列复杂的加工转换成了视图页面,而中间的部分就是我们讨论的重点。
二、渲染时间点
重要的事情说三遍!浏览器是多进程架构,每个进程都有自己明确的分工,从命名上也可以推断出大概的功能。例如网络进程就是专门用于处理网络通信相关,而渲染进程就是专门处理浏览器页面渲染相关。当我们输出url敲下回车,实际上是网络进程去发送的请求。
思考一下:为什么网络线程不直接将HTML抛给渲染主线程?
三、渲染流水线
这张图,很好的体现了整个流水线的工作方式。每一步的输入都是上一步的输出,每一步都严格保证幂等,每一步都不可缺少。下面,我们对每一步骤进行单独拆解,看看每一步都干了啥事?
四、解析HTML(Parse HTML)
在 Parse HTML 这一步骤中,实际上是将 HTML字符串 转换成 DOM树 和 CSSOM树
请大家思考一个问题,在完成网络通讯后实际上浏览器已经拿到了HTML源码,为什么浏览器要如何麻烦的将HTML源码解析成 DOM 树和 CSSOM 树呢?
其中我认为最重要的一个原因是树型结构比字符串更好操作。
这里得到的DOM树,其实就是document,我们可以通过console.dir(document)查看。
这里得到的CSSOM树,其实就是document.styleSheets,可以直接在控制台中查看。
这里额外说一下 CSS样式表(StyleSheetList),实际是是由 作者样式表(外部样式表、内部样式表)、用户样式、浏览器默认样式表 构成,每一个style标签、link标签都是一个CSStyleSheet。注意,内联样式无法不存在于样式表中,而是存在于DOM树中,因为它其实就是元素的style属性。
HTML解析过程中遇到 CSS代码怎么办?
为了提高解析效率,浏览器会启动一个预解析器率先下载和解析CSS,在解析HTML阶段,浏览器是不会去阻塞等待CSS下载完成在继续解析。但是,如果整个解析过程完毕CSS还没有下载完,会阻塞在这里等待,因为下一步骤需要完整的CSSOM和DOM。所以在平时开发时,我们要避免CSS文件太大,而导致请求时间过长阻塞页面渲染。
HTML解析过程中遇到 JS代码怎么办?
浏览器主线程遇到JS时,必须暂时一切行为,等待下载执行完后才能继续。原因是JS可能会修改dom树结构 ,预解析线程可以分担一点下载JS的任务
五、样式计算(Recalculate Style)
这一步骤并不是简单的将两棵树进行合并,而是将元素节点的所有样式进行计算。其中包括:
- 将相对单位(em、rem、%、calc)等转换成绝对单位
- 样式继承
- 将元素的所有样式属性都进行赋值(可以在开发者工具中computed中查看)
- 标记是否需要布局更新(例如宽、高、盒模型属性变化等),此步骤会影响后续布局
六、布局(Layout)
由元素节点的几何信息生成布局树
浏览器会遍历每一个元素节点,计算出它的宽高以及相对包含块的位置信息。这一步骤是为了使用之前计算好样式的dom树来生成布局树。有了布局树后,浏览器可以通过布局树拿到元素节点的几何信息。 这颗布局树是C++对象,但是浏览器通过包裹了一层JS从而暴露出一些我们可以访问的Api,例如 offsetHeight、clientHeight等属性,大家可以回忆下这些属性的值是不是通过浏览器视图窗口计算得出的。
DOM树和Layout树不一定是一一对应的
需要注意的是布局树和dom树不一定是一一对应的,比如 display:none 的元素不会出现在布局树中,因为它没有几何信息(坐标位置、宽高等),简单理解下布局树针对的是浏览器的视图窗口,而dom树则是css属性。
到这里可以知道,凡是会修改元素几何信息的操作都是会引发浏览器的重新布局(也叫reflow)。重新布局非常损耗性能,因为本身Layout阶段会有大量计算工作非常耗时,且之后的流程都得重走一遍。那么日常中我们会有哪些导致重新布局的操作呢?比如通过改变元素的left属性做平移动画、比如我们元素没有设置高度,内容通过接口获取自动撑开高度。
七、分层(Layer)
分层这一步骤的目的实际上是为了优化性能,比如将一个页面分3层,如果第一层内容发生变化,可以单独渲染和合成这一层而不会影响到其他层。例如浏览器的滚动条、fixed和sticky布局的元素等都是会被分配到独立的层中,试想一下,如果不分层,当用户滚动滚动条时浏览器重新渲染整个页面会非常损耗性能。
跟堆叠上下文有关的属性会不同程度上的影响浏览器的分层结果(z-index、opacity、transfrom等等),其中will-change会更大程度的影响分层结果。
八、绘制(Paint)
渲染主线程的工作到此为止,剩余步骤交给其他线程完成。
九、分块(Tiling)
分块会将每一层划分成多个小的区域(思考为什么要这样做?)
分块工作是交给多个线程同时进行的
十、光栅化(Raster)
光栅化是将每个块转换成位图,优先转换靠近视口的块
此过程会用到GPU加速
十一、画(Draw)
合成线程计算出每个位图在屏幕上的位置,交给 GPU 进行最终呈现
transform变形就是在这一步确定的
(思考为什么合成线程不直接交给硬件?)