浏览器笑话90%都和IE有关,没了IE的浏览器世界总归是少很多乐趣😭
浏览器架构
参考资料:现代网络浏览器幕后揭秘
1.浏览器分层结构
- User Interface(用户界面):包括工具栏,地址栏,前进后退按钮,书签等(用户界面还可以与桌面环境集成,以提供浏览器会话管理或与其他桌面应用程序的通信)
- Brower Engine(浏览器引擎):在用户界面和渲染引擎之间传送指令
- Rendering Engine(渲染引擎):负责显示请求的内容(解析HTML和CSS渲染在页面上)
- Networking(网络):用于网络调用如HTTP请求(其接口与平台无关,并为所有平台提供底层实现)
- JavaScript Interpreter(JavaScript解释器):用于接解释和执行JavaScript代码
- XML Parser(XML 解析器):将XML文档解析成文档对象模型树(DOM)
- Display Backend(用户界面后端):用于绘制基本的窗口小部件,比如组合框和窗口(其接口与平台无关,底层使用了操作系统的用户界面方法)
- Data Persistence(数据持久化):浏览器内数据库,将各种数据保存在硬盘上(如书签,工具栏,Cookie,缓存等)
2.浏览器进程架构
参考资料:浏览器工作原理
(1)浏览器多进程
单进程的浏览器需要处理的事情过多,极度不稳定和安全——如果多个页面共享一个进程,单某个tab页崩溃,将导致同进程中的其他页面也会崩溃,影响用户体验
现代浏览器更多采用多进程架构,进程之间不会共享资源和地址空间,所以不会存在太多安全问题,当然,多进程相对于单进程而言,内存等资源的消耗更大
(2)浏览器主要进程
- 浏览器进程 (Browser Process):浏览器主进程(无论打开几个tab,几个弹窗浏览器进程只有一个),负责浏览器的TAB的前进、后退、地址栏、书签栏的工作和处理浏览器的一些不可见的底层操作,比如网络请求和文件访问
- 渲染进程 (Renderer Process):浏览器渲染进程(Render 进程),即通常说的浏览器内核,负责一个Tab内的显示相关的工作(页面渲染、脚本执行、事件处理),每个 tab 页的打开都会创建一个 Render 进程,并且互不影响
- 插件进程 (Plugin Process):负责控制网页使用到的插件(每种类型的插件对应一个进程,仅当使用该插件时才创建)
- GPU进程 (GPU Process):负责处理整个应用程序的GPU任务(网页、Chrome 的 UI 界面都选择采用 GPU 来绘制)
- 网络进程(Network Process):主要负责页面的网络资源加载(在以前的架构中是整合进浏览器进程中作为一个线程,本文的后续内容会将网络这一部分视作线程)
(3)进程模式
浏览器不同的进程模式会对tab进程做不同的处理:
- Process-per-site *:同一个 *site 使用一个进程(site即相同注册域名,比如a.baidu.com和b.baidu.com就可以理解为同一个 site)
- Process-per-site-instance (default) :同一个 site-instance 使用一个进程(site-instance即来自同一站点的连接页面,满足site的连接且通过a标签或js代码打开的新页面)
- *Process-per-tab *: 每个 tab 使用一个进程
- *Single process * :所有 tab 共用一个进程
(4)进程间关系
- 用户在浏览器地址栏输入url,并按下Enter
- 浏览器进程向URL发送请求,获取这个URL的HTML内容并交给渲染进程
- 渲染进程解析HTML内容,解析遇到网络资源再返回来交给浏览器进程进行加载;
- 渲染进程同时还通知浏览器进程去启动插件进程,执行插件代码
- 解析完成后,渲染进程将计算得到的数据帧交给GPU进程,GPU进程将其转换为图像显示到屏幕上
浏览器工作流程
从我们再浏览器键入一个URL地址,到最后网页呈现在浏览器上,经过了那些过程?
1.浏览器页面加载
浏览器进程针对工作的不同有以下的工作线程:
- UI线程:控制浏览器的按钮与输入框
- 网络线程:处理网络请求
- 存储线程:控制文件访问
(1)输入处理
当我们在浏览器的地址栏输入内容按下回车时,UI 线程会根据输入内容判断输入内容是搜索关键字还是URL
如果判定为搜索关键字,则跳转到默认搜索引擎的搜索URL
如果判定为URL,则开始请求URL
(2)开始导航
UI线程将URL交给网络线程,网络线程则负责联系目标主机获取信息(其中发生了DNS域名解析,TLS连接等操作都是计算机网络相关内容)
(3)读取响应
网络线程接收到目标主机的响应后,解析HTTP响应报文,响应分为header(响应相关信息)和payload(真实数据内容)两部分
如果状态码为301或302,则需要取得响应头中Location地址,重新发起请求
如果状态码为200,则可以进行下面的步骤:
浏览器根据响应头中的Content-Type
来确定相应主体的媒体类型(MIME Type):如果是text/html时则将相应数据交给渲染进程来进行下一步工作(解析HTML内容等),image/png
则调用图片渲染器
读取响应的过程中会有以下的安全机制:
- 浏览器并不完全信任
Content-Type
,所以当收到响应主体(payload)时,网络线程会在必要时检查数据的前几个字节,以确保数据内容与 header 里标识的数据类型(Content-Type)一致。如果不一致,那么就需要进行 MIME 类型嗅探来猜测该数据的类型 - 浏览器会进行
Safe Browsing
安全检查,如果域名或者请求内容匹配到已知的恶意站点,network thread 会展示一个警告页 - 网络线程还会做
CORB(Cross Origin Read Blocking)
检查来确定那些敏感的跨站数据不会被发送至渲染进程
(4)查找渲染进程
网络线程确认浏览器可以导航到请求网页后,会通知UI线程去查找渲染进程进行网页的渲染
考虑到网络请求获取响应需要时间,UI线程可以并行查找和启动一个渲染进程,当网络线程接收到数据时,渲染进程就已经准备好了
(5)确认导航
- 数据和渲染进程都准备了,浏览器进程会向渲染进程发送IPC消息(进程间通信)来确认导航
- 浏览器进程将数据发送给渲染进程
- 渲染进程接收到数据后,又发送IPC消息给浏览器进程,表明导航已提交,页面开始加载
- 地址栏会更新,安全指示符更新(地址前面的小锁),访问历史列表(history tab)更新,即可以通过前进后退来切换该页面
(6)初始化加载完成
导航提交后,渲染进程开始加载资源及渲染页面(具体页面渲染原理见下文),页面渲染完成后,会向浏览器进程发送IPC消息,这时UI线程停止展示tab加载中图标
2.浏览器页面渲染
参考资料:
浏览器页面渲染步骤:
浏览器进程把数据交给了渲染进程,渲染进程将HTML/CSS/JS代码,转化为用户可进行交互的web页面
渲染进程包含以下线程:
- 一个主线程(main thread)
- 多个工作线程(work thread)
- 一个合成器线程(compositor thread)
- 多个光栅化线程(raster thread)
浏览器页面渲染进程:
- 渲染进程将HTML内容转换为能够读懂的
DOM树
结构 - 渲染引擎将CSS样式表转化为浏览器可以理解的
styleSheets
,计算出DOM节点的样式。 - 创建
布局树
,并计算元素的布局信息 - 对布局树进行分层,并生成
分层树
- 为每个图层生成
绘制列表
,并将其提交到合成线程 - 合成线程将图层分成
图块
,并在光栅化线程池
中将图块转换成位图 - 合成线程发送绘制图块命令
DrawQuad
给浏览器进程 - 浏览器进程根据DrawQuad消息生成页面,并
显示
到显示器上
(1)构建DOM
渲染进程接受到导航确认信息后,开始接受来自浏览器进程数据(请求响应),渲染进程的主线程解析数据化为DOM对象,
构建DOM过程中:
- 子资源加载:解析到图片,CSS,JS脚本等资源,主线程逐一交给浏览器进程发起请求去获取。为了提升效率,浏览器往往会运行预加载扫描程序,如果html中存在img,link等标签,预加载扫描程序会把这些请求传递给浏览器进程的网络线程去下载
- Javascript的下载与执行:解析遇到
<script>
标签,主线程停止对HTML的解析,而去加载执行JS代码(在<script>
标签添加上async或defer等属性,浏览器会异步加载和执行JS代码,而不会阻塞渲染)
(2)样式计算
- 主线程在解析页面时,遇到
<style>
标签或者<link>
标签的CSS资源,会加载CSS代码 - 根据CSS代码构建styleSheets树
- 样式计算规则:继承规则(当前标签的样式继承了其所有父标签的样式),层叠规则(多个样式同时作用于该标签时,进行样式层叠)
(3)布局
- 对DOM树和styleSheets树进行合并,生成render树(布局树),生成布局树时浏览器会遍历DOM树所有可见的节点添加到布局树中,而不可见的节点会被忽略掉
- 渲染进程还需要计算出每个标签对应的物理位置并存储在render树中
(4)分层
页面上还涉及许多复杂的样式:transform, animation 动画、scroll,z-indexing改变层级等等,浏览器则为这些特殊的节点建立一个对应图层,生成图层树(LayerTree),将这些图层合并在一起,就是一整个页面的样式
分层规则:
- 拥有层叠上下文属性的元素即使用了z-index的元素
- 需要剪裁的地方也会被创建为图层(当父容器的宽高不足以撑起子容器的宽高,出现滚动条或者设置父容器为overflow :hode 等等,子容器页面就会被裁剪)
(5)绘制
将图层拆分成一条条指令,逐条执行绘制图形
(6)分块
当页面内容很长时,页面就会出现滚动条。这时的视口大小有限(在当前屏幕区域能看到的模块就叫视口),在这种情况下要绘制所有图层内容开销太大,所以需要将图层分成很多图块
(7)光栅化
- 渲染进程将这些图层分成很多图块后,然后按照视口附近的图块来通过光栅化优先生成位图(即屏幕上的像素),所以图块是光栅化执行的最小单位
- 当用户滚动页面时,由于页面各个层都已经被光栅化了,浏览器需要做的只是合成一个新的帧来展示滚动后的效果
- 之前的生成DOM树、styleSheets树、render树(Layout)、分层(Layer)、绘制(Paint)都是在渲染引起的主线程中运行的, 绘制列表记录好绘制顺序和绘制指令的列表后,将其提交给渲染引擎中的合成线程
- 合成线程再交给光栅化线程池对图块进行处理
- 光栅化过程往往使用GPU来加速生成即快速光栅化,生成的位图保存在GPU内存中
(8)合成与显示
- 一旦所有图块都被光栅化,合成线程就会生成一个绘制图块的命令——“DrawQuad”,然后将该命令提交给浏览器进程
- 浏览器进程里面有一个叫 viz 的组件,用来接收合成线程发过来的 DrawQuad 命令,然后根据 DrawQuad 命令,将其页面内容绘制到内存中,最后再将内存显示在屏幕上
3.浏览器页面交互
(1)浏览器对事件处理
浏览器进程首先接收到事件信息和事件发生的坐标,随后便把信息传递给渲染进程,渲染进程根据事件发生的坐标找到目标对象(target),然后运行这个目标对象的绑定事件对应的监听函数(listener)
(2)合成线程接收事件
合成线程接收事件的情况主要针对页面滚动相关的事件,合成线程可以独立于主线程之外通过已光栅化的层创建组合帧
当渲染进程中的合成线程接收到事件信息,要进行以下判定:
- 页面合成时,合成器线程会标记页面中绑定有页面滚动事件处理器的区域为非快速滚动区域(non-fast scrollable region)
- 如果事件发生在这些存在标注的区域,合成线程会把事件信息发送给主线程,等待主线程进行事件处理
- 如果事件不是发生在这些区域,合成线程则会直接合成新的帧而不用等到主线程的响应
(3)查找事件的目标对象
当合成线程接收到事件信息,判定到事件发生不在非快速滚动区域后,合成器线程会向主线程发送这个时间信息,主线程获取到事件信息的第一件事就是通过命中测试(hit test)去找到事件的目标对象
具体的命中测试流程是遍历在绘制阶段生成的绘画记录(paint records)来找到包含了事件发生坐标上的元素对象
(4)事件处理优化
为了浏览流畅,浏览器需要保证渲染进程的渲染速度与屏幕刷新率一致(大概每秒 60 帧),但是存在某些事件触发频率超过了这个数值(比如wheel,mousewheel,mousemove,pointermove,touchmove,这些连续性的事件一般每秒会触发60~120次)
事件淹没了屏幕刷新的时间轴,导致页面很卡顿:
假如每一次触发事件都将事件发送到主线程处理,由于屏幕的刷新速率相对来说较低,这样使得主线程会触发过量的命中测试以及JS代码,使得性能有了没必要是损耗
和之前相同的事件轴,可是这次事件被合并并延迟调度了:
浏览器会合并这些连续的事件,延迟到下一帧渲染再执行,达到事件处理优化的目的