浏览器渲染机制(上)

欢迎阅读,本文分上下篇,将详细介绍浏览器进程和线程、JS代码的解析与执行、浏览器的渲染机制。主要想对之前名为《浏览器渲染机制和React虚拟DOM》的一次前端技术分享进行总结和扩充。

前言

作为一名前端开发人员,几乎每天都与浏览器打交道,但你知道我们前端开发最常用的 Chrome 浏览器到底是怎么解析我们书写的 JS 代码的吗?
别着急,在讲解之前,我们需要知道什么是进程和线程。

进程和线程的概念

  1. 进程 是 CPU 资源分配的最小单位(是能拥有资源和独立运行的最小单位)。对于操作系统来说,一个任务就是一个进程,比如打开一个浏览器就是启动了一个浏览器进程,打开一个 Word 就启动了一个 Word 进程。
  2. 线程 是 CPU 调度的最小单位(是建立在进程基础上的一次程序运行单位)。在一个进程内部,要同时做多件事,就需要同时运行多个“子任务”,我们把进程内的这些“子任务”称为线程。

那浏览器的进程是什么样的呢?

浏览器里的进程架构

以 Chrome 为例,它采用多进程架构,Chrome 也是第一个采用多进程架构的浏览器,它的每个进程都有自己核心的职责,它们相互配合完成浏览器的整体功能。其顶层存在一个 Browser process 用以协调浏览器的其它进程。
每个进程中又包含多个线程,一个进程内的多个线程也会协同工作,配合完成所在进程的职责。

该进程架构的优点

  1. 由于新开一个 tab 页面将默认新建一个进程,所以单个 tab 页面崩溃不会影响到整个浏览器
  2. 同样第三方插件崩溃也不会影响到整个浏览器
  3. 多进程可以充分利用现代 CPU 多核的优势
  4. 方便使用 [沙盒/沙箱模型|https://www.cnblogs.com/slly/p/6639173.html] 隔离插件等进程,提高浏览器的稳定性

该进程架构的缺点

  1. 系统为浏览器新开的进程分配内存、CPU 等资源,所以内存和 CPU 的资源消耗也会更大。
    不过不用太担心,Chrome 在内存释放方面做的不错,基本内存都是能很快释放掉给其他程序运行的。

我们已经知道 Chrome 采用多进程,那它究竟有哪些进程,这些进程有分别负责哪些工作呢?

浏览器的主要进程及其职责

1. 主进程

主进程负责浏览器界面的显示与交互,各个页面的管理、创建和销毁其他进程。网络的资源管理、下载等。

2. 第三方插件进程

第三方插件进程每种类型的插件对应一个进程,仅当使用该插件时才创建。

3. GPU 进程

GPU 进程最多只有一个,用于 3D 绘制等。

4. 渲染进程(本篇重点)

渲染进程称为浏览器渲染进程或浏览器内核,其内部是多线程的。主要负责页面渲染,脚本执行,事件处理等。

渲染进程关系的浏览器究竟是怎么把我们书写的代码经过怎样的处理,并展示出来的?前面我们说了,进程可包含很多个线程,渲染进程包含 5 个线程:

4.1 GUI 渲染线程

GUI 渲染线程负责渲染浏览器界面,解析 HTML、CSS、构建 DOM 树和 RenderObject 树、布局和绘制等。
当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行。
这里需要注意,GUI 渲染线程与 JS 引擎线程是互斥的,当 JS 引擎执行时 GUI 线程会被挂起(相当于被冻结了),GUI 更新会被保存在一个队列中,等到 JS 引擎空闲时立即被执行。

4.2 JS 引擎线程

Javascript 引擎,也称为 JS 内核,负责处理 Javascript 脚本程序。(例如 V8 引擎)
JS 引擎线程负责解析 Javascript 脚本,运行代码。
JS 引擎一直等待着任务队列中任务的到来,然后加以处理,一个 Tab 页(renderer 进程)中无论什么时候都只有一个 JS 线程在运行 JS 程序。
这里需要注意,GUI 渲染线程与 JS 引擎线程是互斥的,所以如果 JS 执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。

4.3 事件触发线程

事件触发线程归属于浏览器而不是 JS 引擎,用来控制事件循环(可以理解为:JS 引擎自己都忙不过来,需要浏览器另开线程协助)
当 JS 引擎执行代码块如 setTimeOut 时(也可来自浏览器内核的其他线程,如鼠标点击、AJAX 异步请求等),会将对应任务添加到事件线程中
当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待 JS 引擎的处理
这里需要注意,由于 JS 的单线程关系,所以这些待处理队列中的事件都得排队等待 JS 引擎处理,当 JS 引擎空闲时才会去执行。

4.4 定时触发器线程

传说中的 setInterval 与 setTimeout 所在的线程。
浏览器定时计数器并不是由 JavaScript 引擎计数的,因为 JavaScript 引擎是单线程的,如果处于阻塞线程状态就会影响记计时的准确,因此通过单独线程来计时并触发定时,计时完毕后,添加到事件队列中,等待 JS 引擎空闲后执行。
这里需要注意,W3C 在 HTML 标准中规定,规定要求 setTimeout 中低于 4ms 的时间间隔算为 4ms。

4.5 异步 http 请求线程

XMLHttpRequest 在连接后是通过浏览器新开一个线程请求的。检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中,再由 JavaScript 引擎执行。

浏览器渲染流程(下篇详细讲解)

解析 HTML 文件,构建 DOM 树,同时浏览器主进程负责下载 CSS 文件。
CSS 文件下载完成,解析 CSS 文件成树形的数据结构,然后结合 DOM 树合并成 RenderObject 树。
布局 RenderObject 树 (Layout/reflow),负责 RenderObject 树中的元素的尺寸,位置等计算。
绘制 RenderObject 树 (paint),绘制页面的像素信息。
浏览器主进程将默认的图层和复合图层交给 GPU 进程,GPU 进程再将各个图层合成(composite),最后显示出页面。

可以思考的问题:

Q: 为什么 Javascript 要是单线程的 ?

这是由于 Javascript 这门脚本语言诞生的使命所致!JavaScript 为处理页面中用户的交互,以及操作 DOM 树、CSS 样式树来给用户呈现一份动态而丰富的交互体验和服务器逻辑的交互处理。
如果 JavaScript 使用多线程的方式来操作这些 UI DOM,可能出现 UI 操作的冲突。
如果 Javascript 是多线程的话,在多线程的交互下,处于 UI 中的 DOM 节点就可能成为一个临界资源,
假设存在两个线程同时操作一个 DOM,一个负责修改一个负责删除,那么这个时候就需要浏览器来裁决使用哪个线程的执行结果。当然我们可以通过锁来解决上面的问题,但为了避免因为引入了锁而带来更大的复杂性,Javascript 在最初就选择了单线程执行。