浏览器内核结构图

-
WebCore 是各个浏览器使用的共享部分,包括HTML解析器、CSS解析器、DOM和SVG等
-
JSCore是WebKit的默认引擎,在谷歌系列产品中被替换为V8引擎。Javascript的引擎,或者类比Java的虚拟机
-
WebKit Eembedding API是平台差异的,不同的浏览器实现不同
浏览器工作原理
浏览器的进程结构
在Chrome中,主要的进程有4个:
-
浏览器进程 (Browser Process):负责浏览器的TAB的前进、后退、地址栏、书签栏的工作和处理浏览器的一些不可见的底层操作,比如网络请求和文件访问。
-
渲染进程 (Renderer Process):负责一个Tab内的显示相关的工作,也称渲染引擎。
-
GPU进程 (GPU Process):负责处理整个应用程序的GPU任务。
-
插件进程 (Plugin Process):负责控制网页使用到的插件。

导航过程
在浏览器中输入网址,浏览器都做了哪些工作,简单的说:
首先,当我们是要浏览一个网页,我们会在浏览器的地址栏里输入URL,这个时候Browser Process
会向这个URL发送请求,获取这个URL的HTML内容。
然后将HTML交给Renderer Process
,Renderer Process
解析HTML内容,解析遇到需要请求网络的资源又返回来交给Browser Process
进行加载,同时通知Browser Process
,需要Plugin Process
加载插件资源,执行插件代码。 解析完成后,Renderer Process
计算得到图像帧,并将这些图像帧交给GPU Process
。
最后GPU Process
将其转化为图像显示屏幕。
下面来看看详细过程。
网页加载
浏览器的主进程是browser进程,渲染的工作由render进程与GPU进程处理之外,大部分工作都是bowser进程来负责的。
bowser进程内部分成3个工作线程:
- UI thread:控制浏览器上的按钮及输入框
- network thread:处理网络请求,从网上获取数据,Chrome72以后,已将network thread单独摘成network service process。(通过ps命令确实能看到该进程)
- storage thread: 控制文件等的访问;
-
处理输入
当我们在浏览器的地址栏输入内容按下回车时,UI thread
会判断输入的内容是搜索关键词(search query)还是URL,如果是搜索关键词,跳转至默认搜索引擎对应都搜索URL,如果输入的内容是URL,则开始请求URL。
-
开始导航
回车按下后,UI thread
将关键词搜索对应的URL或输入的URL交给网络线程Network thread
,此时UI线程使Tab前的图标展示为加载中状态,然后网络进程进行一系列诸如DNS寻址,建立TLS连接等操作进行资源请求。
-
读取响应
network thread
接收到服务器的响应后,开始解析HTTP响应报文,然后根据响应头中的Content-Type
字段来确定响应主体的媒体类型(MIME Type),如果媒体类型是一个HTML文件,则将响应数据交给渲染进程(renderer process)来进行下一步的工作,如果是 zip 文件或者其它文件,会把相关数据传输给下载管理器。
在这一步,浏览器会进行 Safe Browsing 安全检查,如果域名或者请求内容匹配到已知的恶意站点,network thread 会展示一个警告页。除此之外,网络线程还会做 CORB(Cross Origin Read Blocking)检查来确定那些敏感的跨站数据不会被发送至渲染进程。
-
查找渲染进程
各种检查完毕以后,network thread 确信浏览器可以导航到请求网页,network thread 会通知 UI thread 数据已经准备好,UI thread 会查找到一个 renderer process 进行网页的渲染。
-
提交导航
据和渲染进程都准备好了,Browser Process
会向 Renderer Process
发送IPC消息来确认导航,此时,浏览器进程将准备好的数据发送给渲染进程,渲染进程接收到数据之后,又发送IPC消息给浏览器进程,告诉浏览器进程导航已经提交了,页面开始加载。
-
初步加载完成
当导航提交完成后,渲染进程开始加载资源及渲染页面(详细内容下文介绍),当页面渲染完成后(页面及内部的iframe都触发了onload事件),会向浏览器进程发送IPC消息,告知浏览器进程,这个时候UI thread会停止展示tab中的加载中图标
整个加载过程如下:

网页渲染
Render进程中,包含线程分别是:
- 一个主线程(main thread)
- 多个工作线程(work thread)
- 一个合成器线程(compositor thread)
- 多个光栅化线程(raster thread)
整个过程如下:
-
构建DOM树
主线程接收到html后,开始解析数据,转换为DOM树
这个过程中,会解析到image、css 、js等资源,会采用预加载扫描的方式,扫描这些标签,并交给Browser进程的Network线程进行资源的下载。
-
样式计算(style)
DOM树是我们页面的结构,在此基础上,需要计算每个元素的样式。计算样式是主线程根据CSS样式选择器(CSS selectors)计算出的每个DOM元素应该具备的具体样式,即使你的页面没有设置任何自定义的样式,浏览器也会提供其默认的样式。
-
布局(layout)
DOM树和计算样式完成后,我们还需要知道每一个节点在页面上的位置,布局(Layout)其实就是找到所有元素的几何关系的过程。
主线程会遍历DOM 及相关元素的计算样式,构建出包含每个元素的页面坐标信息及盒子模型大小的布局树(Render Tree),遍历过程中,会跳过隐藏的元素(display: none),另外,伪元素虽然在DOM上不可见,但是在布局树上是可见的。
-
绘制(panit)
布局 layout 之后,我们知道了不同元素的结构,样式,几何关系,我们要绘制出一个页面,我们要需要知道每个元素的绘制先后顺序,在绘制阶段,主线程会遍历布局树(layout tree),生成一系列的绘画记录(paint records)。绘画记录可以看做是记录各元素绘制先后顺序的笔记。
-
合成(compositing)
文档结构、元素的样式、元素的几何关系、绘画顺序,这些信息我们都有了,这个时候如果要绘制一个页面,我们需要做的是把这些信息转化为显示器中的像素,这个转化的过程,叫做光栅化
(rasterizing)。chrome在这里采用了一种合成的做法。
合成是一种将页面分成若干层,然后分别对它们进行光栅化,最后在一个单独的线程 - 合成线程(compositor thread)里面合并成一个页面的技术。当用户滚动页面时,由于页面各个层都已经被光栅化了,浏览器需要做的只是合成一个新的帧来展示滚动后的效果罢了。

浏览器对事件的处理
以点击事件(click event)为例,让鼠标点击页面时候,首先接受到事件信息的是Browser Process
,但是Browser Process只知道事件发生的类型和发生的位置,具体怎么对这个点击事件进行处理,还是由Tab内的Renderer Process
进行的。Browser Process接受到事件后,随后便把事件的信息传递给了渲染进程,渲染进程会找到根据事件发生的坐标,找到目标对象(target),并且运行这个目标对象的点击事件绑定的监听函数(listener)
-
render进程对事件的处理
合成器线程可以独立于主线程之外通过已光栅化的层创建组合帧,例如页面滚动,如果没有对页面滚动绑定相关的事件,组合器线程可以独立于主线程创建组合帧,如果页面绑定了页面滚动事件,合成器线程会等待主线程进行事件处理后才会创建组合帧。那么,合成器线程是如何判断出这个事件是否需要路由给主线程处理的呢?
由于执行 JS 是主线程的工作,当页面合成时,合成器线程会标记页面中绑定有事件处理器的区域为非快速滚动区域
(non-fast scrollable region),如果事件发生在这些存在标注的区域,合成器线程会把事件信息发送给主线程,等待主线程进行事件处理,如果事件不是发生在这些区域,合成器线程则会直接合成新的帧而不用等到主线程的响应。
这里就涉及到对根元素body绑定事件的问题:
document.body.addEventListener('touchstart', event => {
if (event.target === area) {
event.preventDefault()
}
})
在开发者角度看,这一段代码没什么问题,但是从浏览器角度看,这一段代码给body元素绑定了事件监听器,也就意味着整个页面都被编辑为一个非快速滚动区域,这会使得即使你的页面的某些区域没有绑定任何事件,每次用户触发事件时,合成器线程也需要和主线程通信并等待反馈,流畅的合成器独立处理合成帧的模式就失效了。
这也有办法优化:只需要在事件监听时传递passtive
参数为 true,passtive
会告诉浏览器你既要绑定事件,又要让组合器线程直接跳过主线程的事件处理直接合成创建组合帧。
document.body.addEventListener('touchstart', event => {
if (event.target === area) {
event.preventDefault()
}
}, {passive: true});