一文搞懂浏览器渲染原理

242 阅读10分钟

前言

在前端的面试中,经常会碰到一个老生常谈的问题:在地址栏中输入URL敲下回车后会发生什么?显然,敲下回车之后浏览器会显示出一个页面,但是这并不是理所当然的,只是浏览器在背后帮我们做了很多工作,而这些工作就是本篇文章的核心,浏览器渲染原理。 在正式讲解浏览器渲染原理之前,您需要先了解一个概念,进程和线程

何为进程,何为线程

进程用比较官方的回答就是,进程是操作系统进行资源分配和调度的基本单位。简单理解就是计算机中正在运行的应用程序就是一个进程,每个进程都有一块专属的内存空间,进程和进程之间是相互独立的。

如果说进程是正在执行的程序,那么线程就是程序的执行器。一个进程至少有一个线程,当一个进程启动的时候,会自动创建一个线程,这个线程叫做主线程。如果程序需要同时执行多块代码,那么主线程就会创建出其他线程来帮助主线程完成任务,所以一个进程可以包含多个线程。

浏览器都有哪些进程和线程

我们都知道,浏览器本身就是一个应用程序,当我们双击浏览器图标,将浏览器启动的时候,操作系统会为浏览器开启一个浏览器进程,并为其分配一块专属的内存空间,当得到内存空间后,该进程会开启线程来调度资源,帮助其完成功能。

浏览器内部的工作极其复杂,为了避免功能相互影响(比如一个功能崩了其他功能也接着崩),浏览器主进程会创建出新的进程,每个进程处理的任务各不相同,这些进程占用的内存空间都是互相独立的,它们之间通过IPC机制进行通信,协作完成各种复杂的工作,那么浏览器都有哪些进程呢?我们通过查看任务管理器就可以看到有哪些进程了。

可以看到浏览器开启的进程是很多的,每一个标签页都是一个进程,其中最主要的进程有:

  • 浏览器进程:主要负责界面显示、用户交互、子进程管理等。浏览器进程内部会启动多个线程处理不同的任务。
  • 网络进程:负责加载网络资源。网络进程内部会启动多个线程来处理不同的网络任务。
  • 渲染进程(本章的重点介绍对象):渲染进程最主要的一个线程是渲染主线程(这里也是事件循环发生的地方),其主要职责就是把从网络上下载的HTML、CSS、JavaScript、图片等资源解析为可以显示和用户交互的页面。

渲染流水线

回到本章最开始的问题,在地址栏中输入URL敲下回车后会发生什么?这是一道很经典的面试题,可以从很多方面回答这个问题,这里从浏览器的角度回答这个问题。

当浏览器接收到用户输入的URL地址后,浏览器会将URL转发给网络进程,网络进程发起真正的URL请求,当网络进程接收到HTML文档后,会产生一个渲染任务,并将其传递给渲染主线程的消息队列,在事件循环机制的作用下,渲染主线程会从消息队列中取出这个任务,开启渲染流程。

渲染流程分为多个阶段,分别是:解析HTML、样式计算、布局、分层、绘制、分块、光栅化、画,每个阶段都有明确的输入和输出,上一个阶段的输出会成为下一个阶段的输入,整个流程形成一套严密的渲染流水线。

下面介绍渲染流水线的每一个阶段都做了些什么。

1.解析HTML

渲染的第一步是解析HTML,在解析的过程中遇到CSS就解析CSS,遇到JS就执行JS,为了提高解析效率,渲染主线程在解析之前就开启了一个预解析线程,用于下载外部的CSS文件和JS文件。

当解析到style标签时,它会由渲染主线程直接处理,这个过程是同步的,不会涉及预解析线程,因此会造成HTML解析阻塞。当解析到link标签引入外部的CSS时,预解析线程会下载并解析外部的CSS,这个过程是异步的,主线程不会等待,继续解析后续的HTML,因此link标签不会阻塞HTML解析。

如果主线程解析到script标签的位置,会停止解析HTML,直到JS代码被执行完成。这是因为JS代码的执行过程中可能会修改当前的DOM树,所以DOM树的生成必须暂停。

这一步完成后,会得到DOM树和CSSOM树。

  • DOM树

  • CSSOM树

2.样式计算

经过上一步的HTML解析,我们得到了一颗DOM树和CSSOM树,接下来主线程会遍历DOM树,根据CSSOM树中的样式,依次计算出每个节点的最终样式,称为Computed Style,在这个过程中,很多预设值会变成绝对值,比如red会变成rgb(255, 0, 0);相对单位会变成绝对单位,比如em会变成px。这一步完成后,会得到一颗带有样式的DOM树。

3.布局

布局阶段会遍历这颗带有样式的DOM树,计算出每个节点的几何信息,例如节点的宽高、相对包含块的位置。大部分时候,DOM树和布局树不是一一对应的,比如某个节点设置了display: none,那么这个节点就没有几何信息,也就不会出现在布局树上;又比如某个节点设置了伪元素,虽然DOM树上不存在伪元素的节点,但是伪元素具有几何信息,所以会在布局树上生成。

4.分层

为了提高浏览器的渲染性能,往往会将一个页面分成不同的图层,图层和图层之间相互独立,这样做的好处是,将来某一层改变后,仅会对该层做后续处理,从而提升效率。

可以通过以下操作查看页面的分层。

  • 那么当满足什么情况时,主线程才会为某个节点创建新的图层呢?

    • 对于<video><canvas>元素,浏览器会自动将其转换为独立的层。
    • 还可以通过设置CSS样式触发分层,比如transformpositionopacitywill-change: transform

这一步结束后我们会得到一颗图层树。

5.绘制

分层结束后,我们会得到一颗分层树,主线程会为每个层单独生成绘制指令集并提交给合成线程,用于描述这一层的内容是如何画出来的。

主线程的工作到这里就结束了,剩下步骤就交给合成线程完成。

6.分块

说分块之前,我们先来说一个概念,视口。通常一个页面很大,用户能看到的只有一部分,我们把这一部分区域称为视口

如果一个图层很大,页面要通过滚动到底部才能全部显示。但是通过视口,用户只能看到页面很小的一部分,所以在这种情况下,要一次性绘制完图层的全部内容,会产生很大的开销,且完全没有必要。因此合成线程会对每个图层进行分块,将其划分为更多的小区域,合成线程会从线程池中拿出更多的线程完成分块工作。

7.栅格化

栅格化就是指将图块转化为位图,而图块是栅格化执行的最小单位。渲染进程维护了一个栅格化线程池,所有图块的栅格化都会在线程池内执行,通常,栅格化的过程都会使用GPU进程来加速生成,并优先处理靠近视口区域的图块,使用GPU生成位图的过程叫快速栅格化或GPU栅格化,生成的位图被保存在GPU内存中,运行方式如下:

这里再补充一下位图的概念,位图是一种图像表示方式,它将图像分成许多小的像素点,每个像素点都有一个特定的颜色值。位图是由一系列二进制数据组成的,每个二进制数据表示一个像素点的颜色值。

8.合成和显示

栅格化完成后,合成线程会生成绘制命令(DrawQuad),这些绘制命令会被发送给浏览器进程中的'viz'组件(在Chrome中称为'cc'模块),'viz'组件会根据绘制命令,将各个位图组合起来,并将多个图层进行合并,组合完成后的位图会被发送到GPU,GPU负责将这些数据渲染到屏幕上。

相关概念

有了渲染流水线的基础,我们来谈谈和渲染流水线相关的三个概念—回流(重排)、重绘、合成。

回流

如果通过JS或CSS代码修改元素的几何信息,如width、height、position,就会触发浏览器的重新布局,重新计算布局树,解析之后的一系列子阶段,这个过程叫回流。回流需要更新完整的渲染流水线,所以开销也是最大的。

重绘

当通过JS修改了某个元素的颜色、背景、边框样式等外观属性的改变,没有引起几何位置的变换,就会直接进入绘制,然后执行之后的一系列子阶段,这个过程叫重绘。 相对回流操作,重绘省去了布局和分层阶段,所以执行效率会比回流效率高。

合成

如果更改了一个既不要布局也不要绘制的属性,渲染引擎会跳过布局和绘制,只执行后续的操作,我们把这个过程叫做合成。

总结

浏览器是一个多进程多线程的应用程序,进程与进程之间通过IPC机制进行通信和协作,以完成各种复杂的工作。浏览器进程接收到URL信息后交给网络进程进行网络请求,接收到响应数据后生成渲染任务,交由渲染进程开启后续的渲染流水线,从解析HTML到最后图层的合并,一步步构建出一个完整的页面。浏览器通过这种多进程架构,能够更好地管理资源,提高稳定性和安全性,防止一个进程的崩溃影响到其他进程。