是时候该理解浏览器的渲染流程了😀

93 阅读10分钟

认识浏览器的多进程

  • 浏览器是多进程的
  • 浏览器之所以能够运行,是因为系统给它的进程分配的资源(CPU、memory cache)
  • 可以简单理解,每一个Tab页面就是一个独立的浏览器进程

chrome任务管理器:

浏览器有自己的优化机制,注意到上图有些进程是会合并的,例如打开多个空Tab页,进程会被合并成一个。

浏览器的进程

浏览器是多进程的,通常认为运行浏览器后,会有下列几种类型的进程启动。

  • 浏览器主进程
    • 负责协调主控,只有一个
    • 负责浏览器的界面显示,与用户交互。如前进后退等
    • 将渲染进程渲染后的bitmap,绘制到用户界面上
    • 网络资源的管理,下载等
  • 第三方插件进程
    • 仅当使用插件时,创建对应的插件进程
  • GPU进程
    • 负责3D的绘制,只有一个
  • 渲染进程
    • 通常一个Tab页面对应一个渲染进程
    • 页面渲染,事件处理等

多进程的优点

相比于单进程浏览器,多进程有如下优点:

  • 避免单个页面卡顿影响整个浏览器
  • 避免第三方插件进程一次浏览器
  • 利用现代计算机多核优势
  • 方便沙盒隔离,提高安全性

渲染进程

浏览器的功能是显示页面,与用户交互,这必然离不开渲染进程的工作,接下来说说渲染进程的组成

简单来说,渲染进程下有五类线程

  • GUI渲染线程
    • 负责渲染浏览器界面,解析HTML、CSS,布局和绘制等
    • 当页面需要重绘和重排时候,该进程就会工作
    • GUI线程和JS执行线程互斥
  • JS引擎线程
    • 执行Js代码(例如V8)
    • 一个渲染进程只能有一个JS线程
  • 事件触发线程
    • 归属于浏览器,用于控制事件循环
    • 当JS引擎执行代码如setTimeout时(或鼠标点击等),会将对应任务添加到事件线程中
  • 定时器触发线程
    • 浏览器的计时器功能并非由JS引擎完成,而是单独开线程来计时
    • 计时完毕后,添加回调函数到事件队列中
  • 异步http请求线程
    • XMLHttpRequest连接后新开一个线程请求
    • 如果设置有回调函数,就把函数放入事件队列中

页面的渲染

页面的渲染由渲染进程完成,接下来会详细讲解渲染进程是怎么渲染页面的

构建DOM树

浏览器无法直接理解和使用HTML,需要将HTML转换为浏览器能够理解的结构--DOM树

由上图可见,输入是HTML文件,输出是DOM树

样式计算

样式计算分为3步

把CSS转换为浏览器都理解的结构

CSS样式来源有三种

和HTML一样,浏览器也是无法直接理解这种纯文本的结构,需要转化为styleSheets,我们可以直接从控制台打印看到

标准化CSS属性值

2em、blue、bold这些值,浏览器并不能直接理解其值的含义,需要进行转换

上图长度单位统一转化为px,颜色转为rgb形式,这样浏览器就能够理解了

计算DOM树每个节点的具体样式

现在,假设我们有如下CSS样式

body { font-size: 20px; }
p { color:blue; }
span  { display: none; }
div { font-weight: bold; color:red }
div  p { color:green;}

应用于DOM节点过后,就是:

我们可以在浏览器控制台看到DOM节点具体被应用的样式

布局阶段

现在我们已经成功构建DOM树,并且把CSS样式精确分配到具体的节点中,接下来就是让这些CSS样式生效,产生布局

创建布局树

注意,DOM树是根据HTML结构生成的,其中有可能包含display:none的节点,那么就不应该出现在最后的树中。

所以在显示之前,我们还需要额外的构建一颗只包含可见元素的布局树

根据DOM树和CompoutedStyle构建:

从上图看出,DOM树中所有不可见的节点都没有包含到布局树中。

分层

经过了布局之后,每个元素的具体位置都被计算出来了,但是在绘制页面之前,还需要进行分层阶段

因为页面中有很多复杂的效果,例如3D变化、页面滚动等,为了更方便地实现这些效果,渲染引擎还需要为特定的节点生成专用的图层,并生成一棵树(LayerTree)

原因很简单,假如不分层。例如3D变化,你可能不想因为一个图形的简单旋转而重绘制整个页面(因为所有的都在一个层上),而分层之后,可以只重绘特定的层,大大减少了资源的消耗

而浏览器对页面分层的结果我们也可以直接看到,打开控制台的图层(Layer)即可看到:

所以我们看到的页面是许多的图层叠加在一起,浏览器的页面实际上被分成了很多图层,这些图层叠加后合成了最终的页面

如果我们使用像position、transform等属性应用与DOM节点,那么浏览器就会自动将它提升为一个层

通常情况下,并不是每个节点都包含一个图层(防止资源浪费),所以一个节点如果没有对应的层,那么就属于父节点的图层

提升为层的条件

  1. 层叠上下文的属性的元素会被单独提升为一层

总的来说,比较常见的做法是:

  • positionabsoluterelative,且z-index不为auto的元素
  • postionfixedsticky的元素
  • flex/grid 容器的子元素,且z-index不为auto
  • opacity 小于1的元素
  • 以下属性值不为none的元素
    • transform
    • filter
    • perspective
    • clip-path
    • mask/mask-image/mask-border
  • will-change 为任意属性而该属性在非默认值时会创建层叠上下文的元素

其中解释一下will-change属性,实际上,它的使用频率是很高的,通常用于把节点提升为单独的一层然后提高节省动画性能消耗

<style>
  .app {
    will-change: transform;
    width: 20px;
    transform: rotate(30deg);
  }
</style>

<div class="app">
  Hello
</div>

上面的代码利用了will-change把节点提升为一个层:

实际上由于各个浏览器的实现不一致,即使提升为了层叠上下文也不一定会创建层,例如使用如下代码,就没有层的效果:

<style>
  .app {
    /* will-change: transform; */
    width: 20px;
    transform: rotate(30deg);
  }
</style>

<div class="app">
  Hello
</div>

可以通过will-change属性,确保提升为一个层

  1. 需要被裁剪的地方会被创建图层

最常见的裁剪就是overflow: auto/scroll,出现滚动条

注意是出现了滚动条,进行了裁剪,才会被提升为层,不只是声明overflow,如若没有进行裁剪,依旧不会被提升


图层绘制

进行了分层之后,接下来就是绘制这些图层,你可以打开控制台看看每个图层对应的绘制情况

左边每一条信息对应了右边某一个图层

注意:虽然在图层绘制阶段,出现了绘制两个字,但是没有进行实际的绘制,只是生成绘制列表

绘制列表中是一些指令,指导如何去绘制,通常绘制一个元素需要好几条绘制指令。所以在图层绘制阶段,输出的内容是待绘制列表


栅格化(raster)操作

在讲解栅格化之前,你需要知道,在上面说的页面渲染,都是在渲染进程中完成,而且是在主线程中完成的(也就是GUI渲染线程),接下来的栅格化,会交给合成线程来完成

合成线程没有在最开始渲染进程的组成中提及,但也应该把它视为渲染进程的一部分

当图层的绘制列表准备好之后,主线程会把绘制列表提交给合成线程,那么合成线程是如何工作的呢?

我们要知道,为了节省渲染的资源消耗,浏览器只会绘制视口附近的元素

证明也很简单:

好了,基于这个原因,合成线程会将图层划分为图块,这些图块的大小通常为512*512或者256*256

合成线程会按照视口附件的图块来优先生成位图,实际生成位图的操作是由栅格化来完成的。所谓栅格化,是将图块转化为位图,而图块是栅格化执行的最小单位。渲染进程还维护了一个栅格化线程池,所有的图块栅格化都是在线程池内执行的

从上图可以看到,主线程把绘制列表交由合成线程之后,合成线程通过调用底部的栅格化线程池完成图块转化为位图的操作

通常,栅格化的过程会使用GPU来加速,使用GPU生成位图的过程交过GPU栅格化,生成的位图保存在GPU内存中


合成和显示

一旦所有图块都被光栅化,合成线程会生成一个绘制图块的命令--DrawQuad,去通知浏览器主进程

主浏览器进程里面有一个叫viz的组件,用来接收合成线程发过来的DrawQuad命令,然后将页面内容绘制到内存中

总的说就是合成线程通知主浏览器进程该去GPU内存里面拿位图并显示出来了

宏观来看,GPU、主进程、渲染进程三者由如下关系

总结我们的页面渲染过程,就是:

  • 渲染进程(主进程)将HTML内容转化为可以理解的DOM树结构
  • 渲染进程(主进程)将CSS样式表转化为可以理解的styleSheets,计算出DOM节点的样式
  • 创建布局树,忽略不显示的元素,并计算元素的布局信息
  • 对布局树进行分层,生成分层树
  • 为每个层生成绘制列表,提交给合成线程
  • 合成线程将图层分成图块,并在栅格化线程池中将图块转换成位图
  • 合成线程发送命令给浏览器主进程
  • 浏览器主进程生成页面,并显示到显示器上

重绘、重排、合成?

看完上面这写,我们来说说前端老生常谈的重绘、重排,相信你会有新的理解。

重排

我们所谓重排指的是元素的几何属性发生变化,先看看下图:

可以看出,通过Javascript或者CSS修改元素的集合位置,那么浏览器会触发重新布局,解析之后的一系列子阶段,这个过程就叫重排。无疑,重排需要更新完整的渲染流水线,所以开销也是最大的

重绘

重绘指的是更改元素的绘制属性,看看下图:

从图中可以看出,如果修改了元素的背景颜色,那么布局阶段将不会被执行,因为并没有引起几何位置的变换,所以就直接进入了绘制阶段,然后执行之后的一系列子阶段,这个过程就叫重绘。相较于重排操作,重绘省去了布局和分层阶段,所以执行效率会比重排操作要高一些。

合成

相比于重绘重排,合成对于我们来说比较陌生,但也很好理解,当我们改变了一个既不影响布局,又不影响绘制的属性,渲染引擎将跳过布局和绘制,只执行后续的合成操作,我们把这个过程叫做合成

在上图中,我们使用了 CSS 的 transform 来实现动画效果,这可以避开重排和重绘阶段,直接在非主线程上执行合成动画操作,所以相对于重绘和重排,合成能大大提升绘制效率

参考:time.geekbang.org/column/arti…