1.为什么要去了解浏览器工作原理
- 写出更好的代码。
- 提供更好的用户体验
2.浏览器发展史
- 1991年:Berners Lee 建立了第一代网络浏览器 WorldWideWeb,当时 WorldWideWeb 功能简单,只支持显示文本图片。
- 1993年:Mosaic 问世,这是一种可以同时显示文本和图像的浏览器,一经推出就受到全球用户的欢迎。
- 1994年:网景浏览器发布,它是由曾经参与开发 Mosaic 的人共同创建。虽然网景只能显示简单的静态 html,没有js、css,但依然大受欢迎,获得世界范围内的成功,并占领了绝大多数市场份额(同年也出现了 opear)。
- 1995年:微软发布 IE1.0 与 IE2.0,自此浏览器大战正式打响。
- 1996年:IE3.0 发布并和 window 操作系统集成在一起。网景市场份额达到86%。
- 1998年:网景成立 Mozilla 基金会。
- 1999年:IE 发行后的4年内,在 windows 操作系统的帮助下,IE 逐渐取代了网景浏览器的领导地位,在1999年达到了浏览器市场份额的99%。
- 2003年:苹果发布 Safari 浏览器,该浏览器被包含在所有苹果的操作系统中。
- 2004年:FIrefox1.0 发布,拉开第二次浏览器大战序幕。
- 2005年:苹果开源了 Safari 浏览器的内核 webkit。
- 2008年:谷歌以苹果开源项目 webkit 作为内核创建了一个新的项目 Chromium。在该项目的基础上,谷歌发布了自己的浏览器产品 Chrome。Chrome 发展十分迅速,已经成为了全球最受欢迎的浏览器。
- 2015年:由于 IE 的性能和体验问题,IE 逐渐掉队,2015年微软放弃了IE 推出了基于 webkit 内核的 Edge 浏览器。
- 2020年:微软推出基于 Chromium 内核的全新 Microsoft Edge 浏览器正式版。
3.浏览器结构
虽然浏览器品类众多,但他们提供的功能也都基本类似,框架结构也都大同小异。浏览器结构大致可以如下划分:
- 用户界面:用于展示除标签页窗口之外的其他用户界面内容。
- 浏览器引擎:存在于用户界面与渲染引擎之间,用于在用户界面和渲染引擎之间传递数据。
- 渲染引擎:负责渲染用户请求的页面内容。渲染引擎还可以分为许多模块。
- 网络模块:负责网络请求。
- JS 解析器:解析和执行 js。
- 数据持久层:帮助浏览器存储各种数据,例如 cookie。
4.渲染引擎
渲染引擎可以说是一个浏览器的核心与灵魂,我们往往会把渲染引擎称为浏览器的内核,不同浏览器的内核也不大一样。
- IE:Trident
- Firefox:Gecko
- Safari:Webkit
- Chrome/Opera/Edge:Blink
本文主要以 Chrome 为例,如果不做特殊说明,都是以 Chrome 为例进行讲解浏览器是如何工作的。
5.进程和线程
浏览器是运行在操作系统上的一个应用程序。每个应用程序必须至少启动一个进程来执行其功能。每个程序往往需要运行很多任务,进程会创建一些线程来帮助它去执行这些小的任务。
这里引入了两个概念:进程和线程。
- 进程:进程是操作系统进行资源分配和调度的基本单元,可以申请和拥有计算机资源,进程是程序的基本执行实体。
- 线程:线程是操作系统能够进行运算调度的最小单位,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
具体地说,当我们启动某个程序时,就会创建一个进程来执行任务代码,同时会为该进程分配内存空间,该应用程序的状态都保存在该内存空间里。当应用关闭时,该内存空间就会被回收。进程可以启动更多的进程来执行任务。由于每个进程分配的内存空间是独立的,如果两个进程间需要传递某些数据,则需要通过进程间通信管道IPC来传递。
很多程序都是多进程的结构,这样是为了避免某一个进程卡死。由于进程间相互独立,这样不会影响到整个应用程序。
举个例子,可以把笔记本电脑想象成一个应用程序,外接鼠标是该应用程序的一个进程。如果外接鼠标出了问题,并不会影响继续使用笔记本电脑。
进程可以将任务分成更多细小的任务,然后通过创建多个线程并行执行不同的任务,同一进程下的线程之间是可以直接通信共享数据的。

6.浏览器中的进程
如今的浏览器是多进程结构,但早期的浏览器是单进程的结构。一个进程中大概有页面线程负责页面渲染和展示等,JS 线程执行 JS 代码还有其他各种线程。单进程的结构引发了很多问题:
- 不稳定:其中一个线程的卡死可能会导致整个进程出问题。例如打开一个标签页,其中一个标签页卡死,可能导致整个浏览器无法正常运行。
- 不安全:浏览器之间是可以共享数据的,导致 JS 线程可以随意访问浏览器进程内的所有数据。
- 不流畅:一个进程经常负担太多事情会导致运行效率低下。
因此,为了解决以上的问题,现在采用多进程浏览器结构,根据进程功能不同来拆卸浏览器。
- 浏览器进程:负责控制 Chrome 浏览器除标签页外的用户界面,包括地址栏、书签、后退和前端按钮以及负责与浏览器的其他进程协调工作。
- 网络进程:负责发起接受网络请求。
- GPU进程:负责整个浏览器界面的渲染。
- 插件进程:负责控制网站使用的所有插件,例如 flash。这里的插件并不是指的 Chrome 浏览器市场里的扩展。
- 渲染器进程:负责控制显示 tab 标签内的所有内容,浏览器在默认情况下会为每个标签页都创建一个进程。
7.浏览器的四种进程模型
Chrome 有四种进程模型。
Process-per-site-instance(默认)
默认情况下,Chromium 为用户访问的网站的每个实例创建一个渲染器进程,这样可以确保来自不同的站点的页面是独立呈现的,并且对同一站点的单独访问也是彼此隔离的。简单来说就是访问不同站点和同一站点的不同页面都会创建新的进程。
Process-per-site
表示同一站点使用同一进程。
Process-per-tab
表示同一 tab 里的所有站点使用一个进程
Single process
表示让浏览器引擎和渲染引擎共用一个进程。
各个模式具体的好处和坏处可以自行查阅文档。显而易见,process-per-site-instance 模型会创建更多的进程,占用更多的内存空间,但确实是最安全的。每个 tab 以及 tab 内的每个站点都是相互隔离互不影响的。当其中一个标签页渲染器进程卡死并不会影响其他标签。
8.在浏览器地址栏输入地址时发生什么
8.1.判断地址还是关键词
当在浏览器地址栏输入地址时,浏览器进程的 UI 线程会捕捉输入内容。如果访问的是网址,则 UI 线程会启动一个网络线程来请示 DNS 进行域名解析,接着开始连接服务器获取数据。如果输入的不是地址,而是一串关键词,浏览器会判定为搜索行为,于是会使用默认配置的搜索引擎来查询。
8.2.SafeBrowsing 检查
接下来看看在网络线程获取到数据后会发生什么,首先会通过 SafeBrowsing 检查站点是否是恶意站点,如果是则会提示警告页面,告知该站点有安全问题,浏览器会阻止访问。当然,也可以强行继续访问。
SafeBrowsing 是谷歌内部的一套站点安全系统,通过检测该站点的数据来判断是否安全。比如通过查看该站点的 IP 是否在谷歌的黑名单之内。
8.3.渲染器进程渲染
当返回数据准备完毕并且通过安全校验时,网络线程会通知 UI 线程准备完毕。之后 UI 线程会创建一个渲染器进程(Renderer Thread)来渲染页面。浏览器进程通过 IPC 管道将数据传递给渲染器进程,正式进入渲染流程。
渲染器接收到的数据也就是html,渲染器进程的核心任务就是把 html、css、js、image 等资源渲染成用户可以交互的 web 页面。渲染器进程的主线程将 html 进行解析,构造 DOM 数据结构(也就是文档数据模型——浏览器对页面在其内部的表示形式,是 web 开发程序员可以通过 JS 与之交互的数据结构和 API)。
8.3.1.构造 DOM 树
html 首先经过 tokeniser 标记化,通过记忆法分析将输入的 html 内容解析成多个标记,根据识别后的标记进行 DOM 数构造。在 DOM 树构造过程中会创建 document 对象,然后以 document 的为根节点的 DOM 树不断进行修改向其中添加各种元素。
html 代码中往往会引入一些额外的资源,比如说图片、CSS、JS脚本等,图片和 CSS 这些资源需要通过网络下载或者从缓存中直接加载,它些资源不会阻塞 html 的解析,因为它们不会影响 DOM 的生成。但当 HTML 标解析过程中遇到 script 标签,就会停止 html 解析流程,转而去加载解析并且执行 JS。之所以如此是因为浏览器不知道 JS 是否会改变当前页面的 HTML 结构。如果 JS 代码里用了 document.write()来修改 html,之前的 THML 解析就没有任何意义了。这也就是为什么一起说要把 script 标签放在合适的位置,或者使用async或defer属性来异步加载执行 JS。

在 html 解析完成后会获得一个 DOM Tree,但还不知道 DOM 树上的每个节点应该长什么样子。主线程需要解析 CSS 并确定每个 DOM 节点的计算样式,即使没有提供自定义的 CSS 样式,浏览器也会有默认的样式表。

8.3.2.生成 Layout 树
在知道 DOM 结构和每个节点的样式后,需要知道每个节点需要放在页面上的哪个位置,也就是节点的坐标以及该节点需要占用多大的区域。这个阶段也被称为 layout 布局,主线程通过遍历 DOM 和计算好的样式来生成 Layout Tree。Layout Tree 上的每个节点都记录了 x, y 坐标和边框尺寸。
需要注意的是,DOM Tree 和 Layout Tree 并不是一一对应的,设置了display:none的节点不会出现在 Layout Tree 上。
而在 before 伪类中添加了 content 值的元素,content 里的内容会出现在 Layout Tree 上,不会出现在 DOM 树里。这是因为 DOM 是通过HTML 解析获得并不关系样式,而 Layout Tree 是根据 DOM 和计算好的样式来生成。Layout Tree 是和最后展示在屏幕上的节点是对应的。
8.3.3.确认绘制顺序
接下来还需知道以什么样的顺序绘制节点。举例来说,z-index属性会影响节点绘制的层级关系。如果按照 DOM 的层级结构来绘制页面则会导致错误的渲染。
为了保证在屏幕上展示正确的层级,主线程遍历 Layout Tree 创建一个绘制记录表(Paint Record)。该表记录了绘制的顺序,这个阶段被称为绘制。
8.3.4.栅格化
知道了文档的绘制顺序,要把这些信息转换成像素点,显示在在屏幕上,这种行为被称为栅格化(Rastering)。
01-早期栅格化
Chrome 最早使用了一种很简单的方式,只栅格化用户可视区域(Viewport)的内容,当用户滚动页面时,再栅格化更多的内容来填充缺失的部分。这种方式带来的问题显而易见,会导致展示延迟。
02-合成
随着不断的优化升级,现在的 Chrome 用了一种更复杂的栅格化流程,叫做合成(Composting),合成是一种将页面的各个部分分成多个图层,分别对其栅格化并在合成器线程中单独进行合成页面的技术。
简单来说就是页面所有的元素按照某种规则进行分图层并把图层都栅格化好了,然后只需要把可视区的内容组合成一帧展示给用户即可。

8.3.5.完整流程
- 主线程遍历 Layout Tree 生成 Layer Tree。
- Layer Tree 生成完毕和绘制顺序确认后,主线程将这些信息传递给合成器线程。
- 合成器线程将每个图层栅格化,由于一层可能像页面的整个长度一样大,因此合成器线程将他们切割成许多图块(tiles),然后将每个图块发送给栅格化线程(Raster Thread)。
- 栅格化线程栅格化每个图块,并将他们存储在 GPU 内存中。
- 当图块栅格化完成后,合成器线程将收集称为
draw quads的图块信息,这些信息里记录了图块在内存中的位置和在页面的哪个位置绘制图块的信息。根据这些信息,合成器线程生成了一个合成器帧(Compositior Frame)。 - 合成器 Frame(帧)通过 IPC 传送给浏览器进程,接着浏览器进程将合成器帧传送到 GPU。
- GPU 渲染展示到屏幕上,此时用户可以看到页面的内容。
- 当页面发生变化,比如滚动当前页面,会生成一个新的合成器帧。新的帧再传给 GPU 再次传到屏幕上。
8.3.6.重排和重绘
01-重排
当改变一个元素的尺寸位置属性时,会重新进行样式计算(Computed Style),布局绘制以及后面的所有流程。
02-重绘
当改变某个元素的颜色属性时,不会重新触发布局,但还是会触发样式计算和绘制。
8.3.7.性能优化(重排和重绘)
重排和重绘都会占用主线程,由于JS 也是在主线程运行,就会出现抢占执行时间的问题。如果写了一个不断导致重排重绘的动画,浏览器则需要在每一帧都运行样式,计算布局和绘制的操作。
页面以每秒60帧的刷新率时才不会让用户感觉到页面卡顿,如果在运行动画时还有大量的 JS 任务需要执行,因为布局、绘制和 JS 执行都是在主线程运行的,当在一帧的时间内布局和绘制结束后,如果还有绘制时间,JS 就会拿到主线程的使用权。
如果 JS 执行时间过长,就会导致在下一帧开始时 JS 没有及时归还主线程导致下一帧动画没有按时渲染,就会出现页面动画的卡顿。

01-requestAnimationFrame()
该方法会在每一帧被调用,通过 API 的回调,把 JS 运行任务分成一些更小的任务块(分成每一帧)。在每一帧时间用完前暂停 JS 执行,归还主线程。这样的话,在下一帧开始时主线程就可以按时执行布局和绘制。React 最新的渲染引擎 Reacg Fiber 用到了该 API 做了很多优化。

02-Transform
上文提到栅格化的整个流程不用占用主线程,只在合成器线程和栅格线程中运行,这意味着它无需和 JS 抢夺主线程。
CSS 中有个动画属性叫 transform,通过该属性实现的动画不会经过布局和绘制而是直接运行在合成器线程和栅格化线程中,所以不会受到主线程中 JS 执行的影响。
更重要的是通过 transform 实现的动画由于不需要经过布局绘制、样式计算等操作,所以节省了很多运算时间(方便实现负责的动画)。

9.内容参考
本文是对B站UP主objtube的卢克儿视频的总结,了解不够清楚的可以去看看原视频。