浏览器渲染流程

为什么打开一个页面,会有4个进程?文章中提及了浏览器的几个主要进程,这回来说明一下渲染流程

渲染进程的核心工作是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,由于渲染机制过于复杂,所以渲染模块在执行的过程中会被划分成很多子阶段,通过输入 HTML 经过这些子阶段最后输出像素,我们把这样一个处理流程叫做渲染流水线

按照渲染时间的顺序,流水线可以分为以下几个子阶段:

  • 构建 DOM 树
  • 样式计算
  • 布局
  • 分层
  • 绘制
  • 分块
  • 光栅化
  • 合成

构建 DOM 树

在渲染引擎内部,有个叫 HTML解析器(HTMLParser)的模块,负责将 HTML 字节流转换成 DOM 结构

通过分词器将字节流转换为Token;Token分为Tag Token和文本Token。Tag Token又可分为StartTag和EndTag,对应着标签的开和闭,eg:、

将Token解析为DOM节点(图中的Node),并将DOM节点添加到DOM树中。HTML解析器维护了一个Token栈结构,用来计算节点之间的父子关系。类似于用栈来匹配带括括号的表达式”3*(7+(4÷2))”,不再详细展开

样式计算

样式计算的目的是为了计算出DOM节点中每个元素的具体样式,这一阶段大致可分三步来完成:

  • 把CSS转换为浏览器能够理解的结构
    渲染引擎无法直接理解CSS文件的内容,所以需要将其解析成渲染引擎能够理解的结构,即styleSheets。在浏览器的控制台输入document.styleSheets就可以查看其结构
    CSSOM(CSS Object Model)是一组允许用JavaScript操纵CSS的API。CSSOM有两个作用
    • 提供给JavaScript操作样式表的能力
    • 为布局树的合成提供基础的样式信息
  • 转换样式表中的属性值,使其标准化
    有一些CSS属性,像font-size: 2em、coler: blue、font-weight: bold,不容易被渲染引擎理解,所以需要将所有值转换为渲染引擎容易理解的、标准化的计算值,这个过程就是属性值标准化
  • 计算出DOM树中每个节点的具体样式
    CSS的继承规则和层叠规则

布局

有了 DOM 树和 DOM 树中元素的样式,但还不能显示页面,因为还缺少了 DOM 元素的几何位置信息,计算出 DOM 树中可见元素的几何位置,这个过程叫布局

布局阶段需要完成的两个任务:

  • 创建布局树
    布局树的基本结构就是复制 DOM 树的结构,循环遍历 DOM 树中所有可见节点,并把这些节点加到布局树中,而不可见的节点会被忽略掉(不可见的元素例如标签下的全部内容,被设为display: none的元素)
  • 布局计算

分层

页面中有很多复杂的效果,如3D变换、页面滚动、或者使用z-index做z轴排序等,为了更方便的实现这些效果,渲染引擎还需要为特定的节点生成专用的图层,并生成一颗对应的图层树

通常情况下,并不是布局树的每个节点都包含一个图层,如果一个节点没有对应的层,那么这个节点就从属于父节点的图层

当满足以下条件时,元素就可以被提升为一个单独的图层:

  • 拥有层叠上下文属性的元素会被提升为单独的一层

    从图中可以看出,明确定位属性的元素、定义透明属性的元素、使用 CSS 滤镜的元素等,都拥有层叠上下文属性

  • 需要剪裁的地方也会被创建为图层

绘制

构建完图层树之后,渲染引擎会对图层树中的每个图层进行绘制,把图层的绘制拆分成一个个小的绘制指令,如先画个矩形、再画个边框、画个背景,这些绘制指令组成一个绘制列表

光栅化

光栅化就是按照绘制列表中的指令生成图片,每一个图层都对应一张图片

绘制列表只是用来记录绘制顺序和绘制指令的列表,而实际上绘制操作是由渲染引擎中的合成线程来完成的,当图层的绘制列表准备好之后,主线程会把绘制列表提交给合成线程

结合下图来看下渲染主线程和合成线程之间的关系:

当图层的绘制列表准备好之后,主线程会把该绘制列表提交(commit)给合成线程,那么接下来合成线程是怎么工作的呢?

一个页面通常很大,但用户只能看到其中的一部分,这部分叫做视口,看不到的部分是没有必要绘制出来的,因此,合成线程会把图层分为图块

合成线程会按照视口附近的图块优先生成位图,实际生成位图的操作由栅格化来执行,所谓栅格化,是指将图块转换为位图,而图块是栅格化执行的最小单位

渲染进程维护了一个栅格化的线程池,所有图块栅格化都是在线程池内执行

通常,栅格化过程都会使用GPU来加速生成,使用GPU生成位图的过程叫做快速栅格化,或者GPU栅格化,生成的位图会被保存在GPU中,

合成与显示

在光栅化完成之后,每个图层都会对应一张图片,合成线程会将这些图片合成”一张“图片

一旦所有图块都被光栅化,合成线程就会生成一个绘制图块的命令——“DrawQuad”,然后将该命令提交给浏览器进程。浏览器进程里面有一个叫 viz 的组件,用来接收合成线程发过来的 DrawQuad 命令,然后根据 DrawQuad 命令,将其页面内容绘制到内存中,最后再将内存显示在屏幕上

合成操作是在合成线程上完成,这也就意味着在执行合成操作时,是不会影响到主线程执行的。这也是为什么经常主线程卡住了,但是CSS动画依然能执行的原因

Q:显示器是怎么显示图像的?

A:显示器有固定的刷新频率,通常是60HZ,也就是每秒更新60张图片,更新的图片都来自于显卡中一个叫前缓冲区的地方,显示器会固定读取60次前缓冲区的图像,并将读取到的图像更新到屏幕上

Q:显卡做什么呢?

A:显卡的职责就是合成新图像,并将图像保存到后缓冲区中,一旦显卡把合成的图像写到后缓冲区,系统就会让后缓冲区和前缓冲区互换,这样

参考

浏览器工作原理与实践