一篇文章让你对浏览器的渲染从模糊到清晰!

1,429 阅读9分钟

前言

一直以来,我对于浏览器的渲染进程都是囫囵吞枣般的认知,在自己阅读了几篇优秀的博客后,用更加通俗的语言写下这篇文章,希望大家以及自己对渲染进程有一个彻底的理解。

流程

浏览器的进程

浏览器的进程目前一般分为五种(以chrome为例),渲染流程是其中开发接触频率最多的。

  • 浏览器进程。主要负责界面显示、用户交互、子进程管理,同时提供存储等功能。

  • 渲染进程。核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎和 JavaScript 引擎都是运行在该进程中,默认情况下,Chrome 会为每个 Tab 标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下。

  • GPU 进程。其实,Chrome 刚开始发布的时候是没有 GPU 进程的。而 GPU 的使用初衷是为了实现 3D CSS 的效果,只是随后网页、Chrome 的 UI 界面都选择采用 GPU 来绘制,这使得 GPU 成为浏览器普遍的需求。最后,Chrome 在其多进程架构上也引入了 GPU 进程。

  • 网络进程。主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里面的,直至最近才独立出来,成为一个单独的进程。

  • 插件进程。主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响。

流程简介

  1. 处理 HTML 并构建 DOM 树
  2. 处理 CSS 构建 CSSOM 树
  3. 将 DOM 与 CSSOM 合并成一个渲染树
  4. 计算布局
  5. 图层分层
  6. 绘制并分块
  7. 光栅化

从 HTML 到 DOM

浏览器进程发送字节数据给渲染进程

浏览器通过网络进程拿到数据后,找到对应的渲染进程,并把数据传输进去。这时候渲染进程拿到了最原始的数据,但只是字节数据,尚不能被浏览器使用。

字节数据转为字符

根据字符编码将字节数据解码成字符数据,这时候的数据就是我们敲键盘敲下来的代码了。

字符进一步转化为标记(token)

起初我以为浏览器引擎能直接识别html代码并解析,事实并非如此,而标记就是让编写的html代码成为浏览器引擎能解析的语言。我们可以把标记理解为包含了某个Html标签信息一种数据结构。

节点

这一步,解析器把标记转化为节点,也就是DOM对象里面的各个独立的实体,但还不是最终的状态,因为这些独立个体还没建立起联系

形成DOM树

解析器把各个节点连接起来,整个结构就像一个树一样,也就是我们所说的DOM树总算形成。在这个树结构中,各个节点的父子兄弟关系就能被进程解析了。

po一张前辈的流程图让大家感受更加直观:

image.png

构建CSS树

从html中获取css文件并解析

当解析DOM树过程中遇到这个CSS文件,就会形成阻塞然后去获取并解析CSS文件。

<!DOCTYPE html>
<html>
<head>
    <link rel="stylesheet" type="text/css" media="screen" href="test.css" />
</head>
<body>
 test
</body>
</html>

同样的解析机制,浏览器接收的还是css文件数据的原始字节,然后跟构建DOM树一样的流程去把字节转换为字符-标记-节点,最后形成树结构,也就是我们常谈的CSSOM或CSS树。

但CSS树相比DOM树多了以下对CSS的处理:

  1. 把样式表的属性值标准化,如颜色全转为rgb格式,em/rem转换为px单位,字体的bold转为700

image.png

  1. 继承父节点的样式,补充或者覆盖,如果找不到父节点的样式,会采用浏览器默认的样式

渲染树

现在DOM跟CSSOM都已经准备妥当了,但两棵树都互相不认识,这时候浏览器的布局系统会遍历DOM树的每个节点,并在CSSOM上找到相对应的节点的样式,把两者结合成最终的渲染树(Render Tree),或者叫布局树(Layout Tree)。但并不是所有节点都会被加入到渲染树中。

  1. 不可见的节点(display:none),以及脚本script不会被渲染
  2. head,meta等不可视标签

扒了一张优秀博主的图给大家更直观的感受

image.png

计算布局

在这个过程中,浏览器会计算出每个页面元素的大小跟位置,用于后续的绘制过程。计算方法涉及的知识点更为深奥,我们一般只需要知道这个过程的结果就行,有兴趣的可查阅下面文章作更深的了解。

分层

在绘制页面之前,还有一个分层的步骤,也就是说浏览器的页面会被分成很多图层,而不是只有一个图层包含所有的页面元素。我在用openlayer开发地图板块的时候对此深有体会,像路网跟地图底图就用两个图层分别去操作,路网的层级会比底图高,因此我们才能看到地图上的街道或者道路信息。

image.png

每个节点都属于其中一个图层,如果没有自身对应的图层,那就属于父节点的图层。

image.png

在浏览器的开发者工具的设置中选择more tools的layers,就可以看到截图中的分层展示了。

image.png

绘制

绘制列表

浏览器把图层拆分为一个个小的绘制指令,如图可以看出来,每个指令包括了元素的操作,位置,属性等。按照顺序把指令连起来就是我们所说的绘制列表。

image.png

分块

渲染进程会把准备的绘制列表提交给合成线程,而这个就是专门用于绘制图层的线程。合成线程会把图层划分为更小的图块,先把用户可视范围的图块绘制出来。假如图层过大,譬如通过滚动条下拉很多才能看到完整的页面,这样完全绘制图层消耗的成本过高。

这个过程中涉及了一个首屏加速的概念,当图层比较大的时候,等全部加载完所有消耗更多的资源以及时间,这个时候,浏览器会展示一个低分辨率版本的图片,同时继续进行合成,当图块绘制完成后,再把低分辨率的图片替换为正常的图片,这种现象在日常浏览网页也常会感受到。

光栅化

简而言之,就是把上面所说的图块,转成位图也就是屏幕上的像素(pixel)。

  1. 通常在栅格化的过程,系统会使用GPU来加速,这个过程叫做快速栅格化或GPU栅格化,生成的位图也因此储存在GPU内存中了
  2. 栅格化后,合成线程通过IPC把绘制指令发送到浏览器进程,合成器帧传递给 GPU 进程,然后页面就绘制出来了。

其他

重排与重绘

重绘(Repaint)

重画一部分屏幕,样式改变,元素几何尺寸以及位置不变。

下面这些操作会导致重绘:
改变 colorbackground 相关属性
改变 outline 相关属性
改变 border-radiusvisibilitybox-shadow 等属性

从图可以看出来,重绘并不需要重新生成DOM,布局以及图层,因此重绘的成本较小。

image.png

重排(Reflow):

DOM结构变化,元素几何尺寸改变,需要重新走一遍渲染的流程,成本比重绘高,

下面这些操作会导致重排:

1. 浏览器窗口大小发生变化
2. 元素内容、尺寸、位置、字体大小等发生变化
3. 查询某些属性或者调用某些方法
4. 添加或者删除可见的 DOM 元素

可以看出重排必定会发生重绘,重绘不一定会引发重排

减少重排跟重绘

那么如何减少重排跟重绘呢,也是面试官最会考的知识点之一:

1. 操作 DOM 时,尽量在低层级的 DOM 节点进行操作
2. 不要使用 table 布局,小的改动能让table重新布局
3. 不要频繁操作元素样式,对于静态页面,可以修改类名,而不是样式
4. 避免频繁操作 DOM,可通过创建 documentFragment,在其应用 DOM 操作后,再添加到文档
5. 将元素先设置为 display:none,操作结束后再将其显示
6. 使用visiblity代替display去操作显示隐藏

解析html中遇到Javascript脚本

在解析html文件的时候遇到js,html就会把进程交给Javascript引擎,后者会先把js加载并执行完,再把权力交回给渲染进程继续后面html的解析。至于为什么阻塞html而先执行javascript,我认为是javascript的执行可能会影响html的DOM和CSS构建。

但这样的机制,如果遇到javascript执行时间过长,或者需要网络进程去请求外部js文件,那加载DOM的时间将大大变慢,为了解决这个问题,html加入了两个属性。

  1. async: 先去获取文件,此时渲染进程继续执行,等文件加载完成后,再阻塞渲染并执行JS文件的内容
  2. defer: 先去获取文件,直到渲染进程完成解析html后,再执行JS文件的内容

但必须留意的是,async跟defer只能用于外部script,不适用于html文件内部的script。

同时为了首屏能更快地渲染完成,很多人也习惯把Javascript放在html的后方

总结

虽然在开发过程中,浏览器的一些具体的渲染或者其他进程并不影响我们实际的代码,但对于分析以及优化性能的时候,对渲染原理的把握能有助于我们更好地往前走一步。我看了十几甚至几十篇关于渲染流程的解读,选择了其中三篇自己最喜欢的版本,并尝试用自己的语言去概括,希望能帮到更多的读者。