第二篇介绍了浏览器的解析过程,其中包含构建DOM树、样式计算和构建布局树从输入URL到页面呈现发生了什么?(第二篇)。这一篇就介绍一下页面渲染的过程。
页面渲染的过程有以下几个步骤:
- 建立图层树
- 生成绘制列表
- 生成图块并且栅格化
- 显示器显示内容
建立图层树
在生成布局树之后,浏览器并不会直接绘制页面,因为有一些复杂的场景,比如层叠上下文,那么什么是层叠上下文呢?又么如何控制层叠上下文显示和隐藏呢?
层叠上下文(stacking context),是HTML中一个三维的概念。在CSS2.1规范中,每个盒模型的位置是三维的,分别是平面画布上的X轴,Y轴以及表示层叠的Z轴。一般情况下,元素在页面上沿X轴Y轴平铺,我们察觉不到它们在Z轴上的层叠关系。而一旦元素发生堆叠,这时就能发现某个元素可能覆盖了另一个元素或者被另一个元素覆盖。
如何去控制层叠上下文的显示和隐藏呢?首先,浏览器在构建完布局树之后,会对特定的节点进行分层,构建一颗图层树。那图层树是根据什么来构建的呢?
一般情况下,节点的图层会默认属于父亲节点的图层(这些图层也称为合成层)。那什么时候会提升为一个单独的合成层呢?
有两种情况需要分别讨论,一种是显式合成,一种是隐式合成。
显示合成
显示合成有以下情况:
拥有层叠上下文节点:
一般有以下几种情况:
- 父元素的display属性值为flex|inline-flex,子元素z-index属性值不为auto的时候,子元素为层叠上下文元素;
- 元素的opacity属性值不是1;
- 元素的transform属性值不是none;
- 元素mix-blend-mode属性值不是normal;
- 元素的filter属性值不是none;
- 元素的isolation属性值是isolate;
- will-change指定的属性值为上面任意一个;
- 元素的-webkit-overflow-scrolling属性值设置为touch。
需要剪裁的地方:
比如一个div,你只给他设置 100 * 100 像素的大小,而你在里面放了非常多的文字,那么超出的文字部分就需要被剪裁。当然如果出现了滚动条,那么滚动条会被单独提升为一个图层。
隐式合成
接下来是隐式合成,简单来说就是层叠等级低的节点被提升为单独的图层之后,那么所有层叠等级比它高的节点都会成为一个单独的图层。
这个隐式合成其实隐藏着巨大的风险,如果在一个大型应用中,当一个z-index比较低的元素被提升为单独图层之后,层叠在它上面的的元素统统都会被提升为单独的图层,可能会增加上千个图层,大大增加内存的压力,甚至直接让页面崩溃。这就是层爆炸的原理。
值得注意的是,当需要repaint时,只需要repaint本身,而不会影响到其他的层。
生成绘制列表
接下来渲染引擎会将图层的绘制拆分成一个个绘制指令,比如先画背景、再描绘边框......然后将这些指令按顺序组合成一个待绘制列表,相当于给后面的绘制操作做了一波计划。
生成图块和并且栅格化
现在开始绘制操作,实际上在渲染进程中绘制操作是由专门的线程来完成的,这个线程叫合成线程。
绘制列表准备好了之后,渲染进程的主线程会给合成线程发送commit消息,把绘制列表提交给合成线程。接下来就是合成线程一展宏图的时候啦。
首先,考虑到视口就这么大,当页面非常大的时候,要滑很长时间才能滑到底,如果要一口气全部绘制出来是相当浪费性能的。因此,合成线程要做的第一件事情就是将图层分块。这些块的大小一般不会特别大,通常是 256 * 256 或者 512 * 512 这个规格。这样可以大大加速页面的首屏展示。
因为后面图块数据要进入 GPU 内存,考虑到浏览器内存上传到 GPU 内存的操作比较慢,即使是绘制一部分图块,也可能会耗费大量时间。针对这个问题,Chrome 采用了一个策略: 在首次合成图块时只采用一个低分辨率的图片,这样首屏展示的时候只是展示出低分辨率的图片,这个时候继续进行合成操作,当正常的图块内容绘制完毕后,会将当前低分辨率的图块内容替换。这也是 Chrome 底层优化首屏加载速度的一个手段。
顺便提醒一点,渲染进程中专门维护了一个栅格化线程池,专门负责把图块转换为位图数据。
然后合成线程会选择视口附近的图块,把它交给栅格化线程池生成位图。
生成位图的过程实际上都会使用 GPU 进行加速,生成的位图最后发送给合成线程。
显示器显示内容
栅格化操作完成后,合成线程会生成一个绘制命令,即"DrawQuad",并发送给浏览器进程。
浏览器进程中的viz组件接收到这个命令,根据这个命令,把页面内容绘制到内存,也就是生成了页面,然后把这部分内存发送给显卡。为什么发给显卡呢?我想有必要先聊一聊显示器显示图像的原理。
无论是 PC 显示器还是手机屏幕,都有一个固定的刷新频率,一般是 60 HZ,即 60 帧,也就是一秒更新 60 张图片,一张图片停留的时间约为 16.7 ms。而每次更新的图片都来自显卡的前缓冲区。而显卡接收到浏览器进程传来的页面后,会合成相应的图像,并将图像保存到后缓冲区,然后系统自动将前缓冲区和后缓冲区对换位置,如此循环更新。
看到这里你也就是明白,当某个动画大量占用内存的时候,浏览器生成图像的时候会变慢,图像传送给显卡就会不及时,而显示器还是以不变的频率刷新,因此会出现卡顿,也就是明显的掉帧现象。
总结
到这里浏览器从输入URL到页面呈现的步骤已经走完了。
首先输入URL之后,浏览器会构建请求,然后进行DNS解析,建立TCP连接,发送HTTP请求,然后服务器返回网络响应,然后浏览器就开始构建DOM树,进行样式计算,然后生成布局树,生成布局树之后就会建立图层树,然后生成绘制列表,再生成图块并且进行栅格化,最后把生成的页面发送给显卡进行页面呈现。