浏览器工作原理
浏览器的多进程架构
一个好的程序常常被划分为几个相互独立又彼此配合的模块,浏览器也是如此,以 Chrome 为例,它由多个进程组成,每个进程都有自己核心的职责,它们相互配合完成浏览器的整体功能,每个进程中又包含多个线程,一个进程内的多个线程也会协同工作,配合完成所在进程的职责。
进程(process)和线程(thread)
CPU、进程、线程之间的关系
- 进程是cpu资源分配的最小单位(是能拥有资源和独立运行的最小单位)
- 线程是cpu调度的最小单位(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)
- 不同进程之间也可以通信,不过代价较大
- 单线程与多线程,都是指在一个进程内的单和多
当我们启动一个应用,计算机会创建一个进程,操作系统会为进程分配一部分内存,应用的所有状态都会保存在这块内存中,应用也许还会创建多个线程来辅助工作,这些线程可以共享这部分内存中的数据。如果应用关闭,进程会被终结,操作系统会释放相关内存。
一个进程还可以要求操作系统生成另一个进程来执行不同的任务,系统会为新的进程分配独立的内存,两个进程之间可以使用 IPC (Inter Process Communication)进行通信。很多应用都会采用这样的设计,如果一个工作进程反应迟钝,重启这个进程不会影响应用其它进程的工作。
浏览器的架构
其实如果要开发一个浏览器,它可以是单进程多线程的应用,也可以是使用 IPC 通信的多进程应用。
Chrome 采用多进程架构,其顶层存在一个 Browser process 用以协调浏览器的其它进程。
具体说来,Chrome 的主要进程及其职责如下:
Browser Process: 负责包括地址栏,书签栏,前进后退按钮等部分的工作; 负责处理浏览器的一些不可见的底层操作,比如网络请求和文件访问;
Renderer Process:负责一个 tab 内关于网页呈现的所有事情
Plugin Process:负责控制一个网页用到的所有插件,如 flash
GPU Process:负责处理 GPU 相关的任务
Chrome 多进程架构的优缺点
优点
某一渲染进程出问题不会影响其他进程
更为安全,在系统层面上限定了不同进程的权限
缺点
由于不同进程间的内存不共享,不同进程的内存常常需要包含相同的内容。
为了节省内存,Chrome 限制了最多的进程数,最大进程数量由设备的内存和 CPU 能力决定,当达到这一限制时,新打开的 Tab 会共用之前同一个站点的渲染进程。
渲染进程是如何工作的
渲染进程几乎负责 Tab 内的所有事情,渲染进程的核心目的在于转换 HTML CSS JS 为用户可交互的 web 页面。渲染进程中主要包含以下线程:
- 主线程 Main thread
- 工作线程 Worker thread
- 排版线程 Compositor thread
- 光栅线程 Raster thread
渲染的流程:
- 构建 DOM
当渲染进程接收到导航的确认信息,开始接受 HTML 数据时,主线程会解析文本字符串为 DOM。
- 加载次级的资源
网页中常常包含诸如图片,CSS,JS 等额外的资源,这些资源需要从网络上或者 cache 中获取。主进程可以在构建 DOM 的过程中会逐一请求它们,为了加速 preload scanner 会同时运行,如果在 html 中存在 <img> <link> 等标签,preload scanner 会把这些请求传递给 Browser process 中的 network thread 进行相关资源的下载。
- JS 的下载与执行
当遇到 <script> 标签时,渲染进程会停止解析 HTML,而去加载,解析和执行 JS 代码,停止解析 html 的原因在于 JS 可能会改变 DOM 的结构(使用诸如 document.write()等API)。
不过开发者其实也有多种方式来告知浏览器应对如何应对某个资源,比如说如果在<script> 标签上添加了 async 或 defer 等属性,浏览器会异步的加载和执行JS代码,而不会阻塞渲染。
- 样式计算
仅仅渲染 DOM 还不足以获知页面的具体样式,主进程还会基于 CSS 选择器解析 CSS 获取每一个节点的最终的计算样式值。即使不提供任何 CSS,浏览器对每个元素也会有一个默认的样式。
- 获取布局
想要渲染一个完整的页面,除了获知每个节点的具体样式,还需要获知每一个节点在页面上的位置,布局其实是找到所有元素的几何关系的过程。其具体过程如下:
通过遍历 DOM 及相关元素的计算样式,主线程会构建出包含每个元素的坐标信息及盒子大小的布局树。布局树和 DOM 树类似,但是其中只包含页面可见的元素,如果一个元素设置了 display:none ,这个元素不会出现在布局树上,伪元素虽然在 DOM 树上不可见,但是在布局树上是可见的。
- 绘制各元素
即使知道了不同元素的位置及样式信息,我们还需要知道不同元素的绘制先后顺序才能正确绘制出整个页面。在绘制阶段,主线程会遍历布局树以创建绘制记录。绘制记录可以看做是记录各元素绘制先后顺序的笔记。
- 合成帧
熟悉 PS 等绘图软件的童鞋肯定对图层这一概念不陌生,现代 Chrome 其实利用了这一概念来组合不同的层。
复合是一种分割页面为不同的层,并单独栅格化,随后组合为帧的技术。不同层的组合由 compositor 线程(合成器线程)完成。
主线程会遍历布局树来创建层树(layer tree),添加了 will-change CSS 属性的元素,会被看做单独的一层,你可能会想给每一个元素都添加上 will-change,不过组合过多的层也许会比在每一帧都栅格化页面中的某些小部分更慢。
一旦层树被创建,渲染顺序被确定,主线程会把这些信息通知给合成器线程,合成器线程会栅格化每一层。有的层的可以达到整个页面的大小,因此,合成器线程将它们分成多个磁贴,并将每个磁贴发送到栅格线程,栅格线程会栅格化每一个磁贴并存储在 GPU 显存中。
一旦磁贴被光栅化,合成器线程会收集称为绘制四边形的磁贴信息以创建合成帧。
合成帧随后会通过 IPC 消息传递给浏览器进程,由于浏览器的 UI 改变或者其它拓展的渲染进程也可以添加合成帧,这些合成帧会被传递给 GPU 用以展示在屏幕上,如果滚动发生,合成器线程会创建另一个合成帧发送给 GPU。
合成器的优点在于,其工作无关主线程,合成器线程不需要等待样式计算或者 JS 执行,这就是为什么合成器相关的动画 最流畅,如果某个动画涉及到布局或者绘制的调整,就会涉及到主线程的重新计算,自然会慢很多。
浏览器对事件的处理
在浏览器的看来,用户的所有手势都是输入,鼠标滚动,悬置,点击等等都是。
当用户在屏幕上触发诸如 touch 等手势时,首先收到手势信息的是 Browser process, 不过 Browser process 只会感知到在哪里发生了手势,对 tab 内内容的处理是还是由渲染进程控制的。
事件发生时,浏览器进程会发送事件类型及相应的坐标给渲染进程,渲染进程随后找到事件对象并执行所有绑定在其上的相关事件处理函数。
前文中,我们提到过合成器可以独立于主线程之外通过合成栅格化层平滑的处理滚动。如果页面中没有绑定相关事件,组合器线程可以独立于主线程创建组合帧。如果页面绑定了相关事件处理器,主线程就不得不出来工作了。这时候合成器线程会怎么处理呢?
这里涉及到一个专业名词「理解非快速滚动区域(non-fast scrollable region)」由于执行 JS 是主线程的工作,当页面合成时,合成器线程会标记页面中绑定有事件处理器的区域为 non-fast scrollable region ,如果存在这个标注,合成器线程会把发生在此处的事件发送给主线程,如果事件不是发生在这些区域,合成器线程则会直接合成新的帧而不用等到主线程的响应。
查找到事件对象
当组合器线程发送输入事件给主线程时,主线程首先会进行命中测试(hit test)来查找对应的事件目标,命中测试会基于渲染过程中生成的绘制记录( paint records )查找事件发生坐标下存在的元素。
事件的优化
一般我们屏幕的刷新速率为 60fps,但是某些事件的触发量会不止这个值,出于优化的目的,Chrome 会合并连续的事件(如 wheel, mousewheel, mousemove, pointermove, touchmove ),并延迟到下一帧渲染时候执行 。
而如 keydown, keyup, mouseup, mousedown, touchstart, 和 touchend 等非连续性事件则会立即被触发。
合并事件虽然能提示性能,但是如果你的应用是绘画等,则很难绘制一条平滑的曲线了,此时可以使用 getCoalescedEvents API 来获取组合的事件。