浏览器工作原理
前置知识
(1) 浏览器的组成结构
浏览器一般由七个模块组成:
用户界面( User Interface )
包括地址栏、后退/前进按钮、书签目录等,也就是除了标签页窗口之外的其他部分。
浏览器引擎( Browser Engine )
可以在用户界面和渲染引擎之间传送指令或在客户端本地缓存中读写数据等,是浏览器中各个部分之间相互通信的核心。
渲染引擎( Rendering Engine )
渲染引擎负责渲染用户请求的页面内容。在渲染引擎下还有很多小的功能模块,比如网络模块、JS 解释器等。
网络( Networking )
用来完成网络调用或资源下载的模块。
JS 解释器( JavaScript Interpreter )
用来解释执行 JS 脚本的模块。
UI 后端( UI Backend )
用来绘制基本的浏览器窗口内控件,如输入框、按钮、单选按钮等,根据浏览器不同绘制的视觉效果也不同,但功能都是一样的。
数据持久化存储( Date Persistence )
浏览器在硬盘中保存 cookie、localStorage 等各种数据,可通过浏览器引擎提供的 API 进行调用。
从浏览器的组成结构来讲,我们常说的浏览器内核,指的就是渲染引擎。
(2) 浏览器的多进程结构
1) 进程/线程
进程是资源分配的最小单位,线程是 CPU 调度的最小单位。
早期浏览器是单进程的,单进程导致了许多问题:
不稳定
一个页面卡死会导致整个浏览器不能正常使用
不安全
浏览器之间共享数据
不流畅
一个进程负责太多的事情,效率低
故现在的浏览器采用了多进程结构
2) 浏览器的进程
浏览器的主要进程有:(以谷歌浏览器为例)
浏览器进程( Browser process )
控制浏览器除标签页外的用户界面,包括地址栏、书签、后退和前进按钮,以及负责和浏览器的其他进程协调工作。
插件进程( Plugin process )
控制网站所使用的所有插件,如 Flash。
渲染进程( Renderer process )
控制显示 tab 标签页内的所有内容,主要作用为页面渲染,脚本执行,事件处理等。浏览器在默认情况下会为每个标签页创建一个进程(这取决于浏览器选择的进程模型)
渲染进程是多线程的:
JS 引擎线程
负责处理 Javascript 脚本程序。
GUI 渲染线程
负责渲染标签页内容,解析 HTML、CSS,构建 DOM 树,布局和绘制等。
事件触发线程
主要负责将准备好的事件交给 JS 引擎线程执行,比如 setTimeout 定时器计数结束,ajax 等异步请求成功并触发回调函数,或者用户触发点击事件时,该线程会将整装待发的事件依次加入到任务队列的尾部,等待 JS 引擎线程的执行。
定时触发器线程
负责执行异步定时器一类的函数的线程,如 setInterval,setTimeout 等。
异步 http 请求线程
负责异步请求一类的函数的线程,如 Promise,axios,ajax 等。
…
GPU 进程( GPU process )
负责整个浏览器界面的渲染。
其实,Chrome 刚开始发布的时候是没有 GPU 进程的。而 GPU 的使用初衷是为了实现 3D CSS 的效果,只是随后网页、Chrome 的 UI 界面都选择采用 GPU 来绘制,这使得 GPU 成为浏览器普遍的需求。最后,Chrome 在其多进程架构上也引入了 GPU 进程。
…
从浏览器的进程角度来讲,浏览器内核指的是渲染进程。
从浏览器输入 URL 开始,到页面渲染完成,浏览器做了哪些事情?
这是一个经典问题,也是作为前端程序员必须要掌握和理解的知识点。
整个过程可以分为以下几步:
- DNS 域名解析
- 建立 TCP 连接
- 发送 HTTP 请求
- 服务器处理请求并返回响应
- 浏览器解析并渲染页面
- 断开 TCP 连接
接下来我们展开说说,以谷歌浏览器为例。
1.DNS 域名解析
浏览器进程的UI 线程会捕捉输入框输入的内容,如果是网址,则 UI 线程会启动一个网络线程请求 DNS 进行域名解析;如果输入的不是网址而是关键字,就会使用默认配置的搜索引擎来查询。
我们重点关注 DNS 域名解析。
(1) 域名结构
以www.bilibili.com
为例,我们通常认为它就是一个域名,但从严格意义上来讲,bilibili.com
才是域名,www
是服务器名,它表示在bilibili.com
域名下,有一台叫做www
的服务器。服务器名.域名
称为完全限定域名,或者叫主机名。一个域名下可以有多个服务器,比如除了www
外,bilibili.com
域名下还有mail
、space
等服务器。
域名是由.
进行划分的,bilibili.com
中bilibili
为二级域名,它又受com
域名管理。com
域名又叫做顶级域名,常见的顶级域名还有cn
、edu
等。那这些顶级域名又受谁管理呢?其实www.bilibili.com
的完整写法应该是www.bilibili.com.root
,或者简写成www.bilibili.com.
,我们把最后这个.
称为根域名,只不过一般我们会把这个.
省略。
所以一个主机名的完整结构是:
1 |
|
有些人会把主机名也当做是域名,把服务器名也当做是一级域。所以会产生 www.baidu.com 究竟是二级域名还是三级域名的讨论。这就看个人理解了。如果你认为 baidu.com 才是域名,www 是服务器名,那它就是二级域名;如果认为 www.baidu.com 是域名,那它就是三级域名。
(2) DNS
DNS(Domain Name System) :域名系统。我们知道每一台主机都有一个 IP 地址,浏览器要想向输入的 URL 的主机名所对应的服务器发送请求,那就需要知道服务器的 IP 地址。DNS 的作用就是将主机名转换成 IP 地址。
DNS 是一个由分层的 DNS 服务器实现的分布式数据库,整个系统由分散在世界各地的许多台 DNS 服务器组成,每台 DNS 服务器上都保存了一些数据,这些数据能够让我们最终查询到主机名对应的 IP。
所以,DNS 域名解析,本质上就是去向 DNS 服务器查询 IP 地址。
(3) DNS 服务器
DNS 服务器,也叫做域名服务器。
它有 3 种类型:
根域名服务器
它的作用就是管理下一级,也就是顶级域名服务器。通过查询根域名服务器,我们可以知道一个主机名所对应的顶级域名服务器 IP 是多少,再继续向顶级域名服务器发起查询请求。
顶级域名服务器
_Top Level Domain(TLD)_:顶级域名服务器。除了刚刚提到的
com
外,常见的顶级域名还有cn
、org
、edu
等。顶级域名服务器提供了下一级权威域名服务器的 IP 地址。权威域名服务器
权威域名服务器管理自己域名下主机(服务器)的 IP 地址,最终可以返回
主机名 - IP
的映射。
层次结构图:
除了上述讲到的三种类型的 DNS 服务器,还有一个本地域名服务器,但是严格来讲,本地域名服务器并不属于 DNS 服务器的层次结构,但是它对 DNS 有着重要作用。当主机发起 DNS 请求时,该请求会被发送到本地域名服务器,本地域名服务器起着代理的作用,负责将该请求转发到 DNS 服务器的层次结构中。
下面我们还是用一个例子展示 DNS 的查询过程。
(4) 查询过程
假设想要获取www.bilibili.com
的 IP 地址。
首先主机会向本地域名服务器发送一个 DNS 查询报文,其中包含了需要被转换的主机名
www.bilibili.com
。本地域名服务器将该报文转发到根域名服务器。
注意:根域名服务器不止一台,全球共有 13 台根域名服务器,本地域名服务器会找最近的根域名服务器。
根域名服务器注意到该主机名的
com
前缀,就会向本地域名服务器返回com
所对应的顶级域名服务器的 IP 地址列表。意思就是,我并不知道
www.bilibili.com
的 IP,不过这些顶级域名服务器可能知道,你去问下他们吧。本地域名服务器就向那些顶级域名服务器发送查询报文。
顶级域名服务器注意到了
bilibili.com
的前缀,就会向本地域名服务器返回对应的权威域名服务器的 IP 地址列表。意思就是,我并不知道
www.bilibili.com
的 IP,不过这些权威域名服务器可能知道,你去问下他们吧。本地域名服务器就向那些权威域名服务器发送查询报文。
最终在某一权威服务器中找到并返回
www.bilibili.com
的 IP 地址。注意:如果域名被注册,必然能在域名服务器中找到对应的 IP 地址。
如图所示,本地域名服务器向其他域名服务器发送查询请求的方式,就是迭代查询,所有请求都是由本地域名服务器发出,并且所有的响应都是直接返回给本地域名服务器。
还有另外一种查询方式叫做递归查询,如图:
响应结果并不直接返回给本地域名服务器,而是由当前域名服务器向下一级域名服务器继续查找,直到找到目标 IP 地址,再逐级返回。
(5) DNS 缓存
为了更快地获得 IP,DNS 广泛使用了缓存技术。
- 在本地 DNS 服务器向根 DNS 服务器查询请求前,它会先去浏览器自身的 DNS 缓存中查找,如果存在,则解析结束。
- 如果浏览器自身的 DNS 缓存中没有,那么会尝试去读取操作系统中的 hosts 文件,看看是否有对应的映射关系,如果存在,则解析结束。
- 如果本地 hosts 文件中没有,则去查找本地 DNS 服务器(ISP 服务器,或者自己手动设置的 DNS 服务器)中的 DNS 缓存,如果存在,则解析结束。
- 如果上述三步中都不存在相应缓存,就开始进行查询请求。
2.建立 TCP 连接
通过 DNS 域名解析,获取到目标 IP 地址后,需要和其建立 TCP 连接,也就是我们常说的三次握手。
(1) 格式
TCP 头部格式:
其中有 6 个标志位:
- SYN(synchronous 建立联机)
- ACK(acknowledgement 确认)
- PSH(push 传送)
- FIN(finish 结束)
- RST(reset 重置)
- URG(urgent 紧急)
(2) 三次握手
SYN 连接请求(客户端)
主机 A 发送
seq=x,SYN=1
的数据包给主机 B,其中seq=x
表示这条数据包的序号。这就是第一次握手,由客户端发出,服务端接收。
SYN、ACK 确认(服务端)
主机 B 接收到后根据
SYN=1
知道了 A 要求建立连接。向 A 发送seq=y,ack=x+1,SYN=1,ACK=1
的数据包,其中seq=y
表示这条数据包的序号,ack=x+1
表示这条数据包是对主机 A 之前发送的seq=x
的数据包的确认,只有标志位ACK=1
时,这个确认序列号,也就是ack
才是有效的。相当于告诉主机 A 我已经准备好了。这就是第二次握手,由服务端发出,客户端接收。
ACK 确认(客户端)
主机 A 收到后,检查
ACK
是否为 1,如果是,继续检查ack
是否正确,即第一次发送数据包的seq+1
,同时检查SYN
是否为 1,如果都满足,则再次发送一条seq=x+1,ack=y+1,ACK=1
的数据包,其中seq=x+1
表示这条数据包的序号,ack=y+1
表示这条数据包是对主机 B 返回的seq=y
的数据包的确认。主机 B 收到后,检查ACK、ack
是否正确,如果正确则连接建立成功。这就是第三次握手,由客户端发出,服务端接收。
为什么要三次握手?
其实这是由 TCP 自身可靠传输的特点决定的。客户端和服务端要进行可靠传输,那么就需要确认双方的接收和发送能力。第一次握手可以确认客户端的发送能力,第二次握手,确认了服务端的发送能力和接收能力,所以第三次握手才可以确认客户端的接收能力。不然容易出现丢包的现象。
3.发送 HTTP 请求
在成功和服务端建立连接之后,就可以发送 http 请求了。
(1) 格式
请求报文:
1 |
|
响应报文:
1 |
|
(2) 状态码
状态码 | 含义 |
---|---|
1xx | 服务器收到请求 |
2xx | 请求成功 |
3xx | 重定向 |
4xx | 客户端错误 |
5xx | 服务端错误 |
(3) HTTP 缓存
什么是 HTTP 缓存?当客户端向服务端请求资源时,会先去缓存中找,如果缓存中存在该资源的副本,则直接从缓存中提取而不是去向服务端请求。
为什么需要 HTTP 缓存?因为网络请求相比较于 CPU 的计算和页面渲染是非常慢的。使用缓存可以加快页面加载速度,同时减少服务器的负担。
哪些资源可以被缓存?静态资源,比如 js、css、图片等。
HTTP 缓存和 DNS 缓存是不一样的,DNS 缓存记录的是主机名到 IP 的映射关系,HTTP 缓存的是静态资源。除此之外,还有一个浏览器缓存,即 Cookie、SessionStorage、LocalStorage 等,不过这不是本文重点,感兴趣的可以另行查看。
HTTP 缓存又两种类型:
- 强缓存
- 协商缓存
下面我们依次介绍
1) 强缓存
强缓存就是向浏览器 HTTP 缓存查找该请求结果,并根据该结果的缓存规则来决定是否使用该结果的过程。
简单来讲,就是去设置资源的有效时间,当再次请求相同资源时,如果缓存仍然有效,直接从缓存中读取资源。
强缓存分了两种方式:Expires
和Cache-Control
- Expries
- 版本:HTTP/1.0
- 来源:存在于服务端返回的响应头中
- 语法:Expires: Wed, 22 Nov 2019 08:41:00 GMT
- 缺点:服务器的时间和浏览器的时间可能并不一致导致失效
- Cache-Control
- 版本:HTTP/1.1
- 来源:响应头和请求头
- 语法:Cache-Control:max-age=3600
当前 HTTP 版本为 1.1,所以强缓存更多的是采用 _Cache-Control_,我们重点来聊 _Cache-Control_。它的具体表现就是在请求头和响应头中添加了 Cache-Control 字段,用来判断该资源的缓存规则。
该字段常见的值如下:
值 | 说明 |
---|---|
max-age=delta-seconds | 缓存最大过期时间为 delta-seconds 秒 |
no-cache | 客户端可以存储资源,但是每次都要去和服务端做新鲜度校验,来决定是重新获取还是直接使用缓存 |
no-store | 永远不在客户端存储资源,永远都是去原始服务器去获取资源 |
注意:虽然请求头和响应头中都能设置 Cache-Control 字段,但一般是响应头发挥作用,比如请求头设置 max-age 为 60s,响应头设置为 30s,最后结果是 30s 缓存就失效了,也就是说服务端的设置决定了缓存的有效时间,另外,只有服务端有能力开启缓存,如果请求头设置 _Cache-Control_,而服务端不设置,缓存是不生效的。那么请求头中的 Cache-Control 有什么用呢?只有请求头中设置了 Cache-Control 值为 no-store 或者 no-cache 或者 max-age=0,也就是客户端不想要走强缓存,那这个 Cache-Control 才是有用的。
2) 协商缓存
协商缓存就是在强制缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识是否决定使用缓存的过程。
简单来讲,就是当强缓存失效时,我们需要去判断缓存中的资源是否仍然有效,如果仍然有效,依旧从缓存中读取资源。
协商缓存也有两种:
Last-Modified / if-Modified-Since
- 意义:资源最后修改时间
- 来源:Last-Modified 在响应头中,if-Modified-Since 在请求头中
- 判断:如果这两个值相同,表示资源并没有更新过,返回 304;不一致则表示资源更新过,就返回 200 和新的资源以及新的 Last-Modified
Etag / if-None-Match
- 意义:资源的唯一标识(一个字符串,类似于人类的指纹)
- 来源:Etag 在响应头中,if-None-Match 在请求头中
- 判断: 如果这两个值相同,表示资源并没有更新过,返回 304;不一致则表示资源更新过,就返回 200 和新的资源以及新的 Etag
那么这两种方式有什么区别呢?其实 Etag 和 Last-Modified 判断资源的方式是一样,只不过后者是一个时间,前者是对资源按照一定方式计算出来的唯一标识,如果资源发生了更新,这个唯一标识必然会有变化。
两者比较:
- 优先使用 Etag
- Last-Modified 只能精确到秒
- 如果资源重复生成,但内容不变,使用 Etag 更精确
3) 综合流程
4) 页面刷新对 HTTP 缓存的影响
有三中刷新类型:
正常操作:浏览器输入 url、连接跳转、前进后退
强制缓存和协商缓存都有效
手动刷新:f5、点击刷新按钮、右键菜单刷新
强制缓存失效,协商缓存有效
强制刷新:ctrl + f5、shift + command + r
强制缓存和协商缓存都失效
4.服务器处理请求并返回响应
每台服务器上都会安装处理请求的应用—— Web Server 。常见的 Web Server 产品有 apache
、nginx
、IIS
或 Lighttpd
等。
HTTP 请求一般可以分为两类,静态资源和动态资源。
请求访问静态资源,这个就直接根据 url 地址去服务器里找就好了。
请求动态资源的话,就需要 web server 把不同请求,委托给服务器上处理相应请求的程序进行处理,然后返回后台程序处理产生的结果作为响应,发送到客户端。
5.浏览器解析并渲染页面
当网络线程获取到数据后,终于要开始渲染页面了。
(如果是谷歌浏览器,当网络线程获取到数据后,需要通过SafeBrowsing检查站点是否是恶意站点,SafeBrowsing 是谷歌内部的一套站点安全系统,通过检测该站点的数据来判断是否安全,通过安全校验后,才进入渲染流程)
(1) 渲染流程
浏览器进程会启动一个渲染进程,并将数据(也就是 html)通过IPC 管道传递给渲染进程,正式开始渲染流程。
渲染进程的核心任务就是把 html、css、js、图片等资源渲染成用户能交互的 web 页面。
渲染进程的主线程会将 html 进行解析,构造DOM 数据结构,html 首先通过Tokeniser 标记化,通过语法分析将 html 内容解析成多个标记,根据识别后的标记进行DOM 树构造,DOM 树构造过程中会创建document 对象,然后以 document 对象为根节点的 DOM 树不断进行修改,向其中添加各种元素。
html 中引入的其他资源,如图片、css、js 等,图片和 CSS 等需要通过http 请求下载或者从http 缓存中直接加载,这些资源不会阻塞 html 的解析,因为它们不会影响 DOM 的生成,但如果解析过程中遇到 script 标签,就会暂停解析,先去加载解析并执行 js 脚本,因为 js 中可能会改变当前页面 html 结构。或者使用 async 或者 defer 属性来异步加载执行 js。
主线程就是指 JS 引擎线程和 GUI 渲染线程,这两个线程是互斥的,JS 引擎线程执行时,GUI 渲染线程不执行,反之亦然。
DOM 树构建完毕后,主线程需要解析 CSS 并确定每个 DOM 节点的计算样式。
即使你没有自定义样式,浏览器也会有自己的默认样式表。
在知道 DOM 结构和每个节点的样式后,我们接下来需要知道每个节点放在页面上的哪个位置,也就是节点的坐标以及该节点需要占用多大的区域,这一阶段叫做Layout 布局。主线程通过遍历 DOM 树和计算好的样式来生成Layout 树,Layout 树上的每个节点都记录了 x,y 坐标和边框尺寸。
DOM 树和 Layout 树并不是一一对应的,如设置了 display:none 的元素不会出现在 Layout 树中,而在 before 伪元素中添加了 content 值的元素,content 中的内容会出现在 Layout 树中,不会出现在 DOM 树中。这是因为 DOM 树是根据解析 html 所得,并不关心样式;而 Layout 树是根据 DOM 节点和计算好的样式来生成,和最终展示在页面上的节点是对应的。
Layout 树创建完毕后,我们还需要知道这些元素要以什么样的顺序进行绘制(比如 z-index 就会影响绘制顺序),主线程遍历 Layout 树,创建一个绘制记录表,该表记录了绘制的顺序,这个阶段称为**绘制(Paint)**。
现在知道了元素的绘制顺序,就到了需要把这些信息真正转化成像素点,显示到屏幕上的时候了,这个阶段称为**栅格化(光栅化)**。
主线程遍历 Layout 树,生成Layer(图层)树,将这些信息传递给合成器线程,合成器线程将每个图层栅格化,生成合成器帧。
早期的 Chrome 栅格化方案:只栅格化页面显示的内容,当页面滚动时,再栅格化更多的内容来填充缺失的部分,这种方式会导致展示延迟。
现在的 Chrome 采用更为复杂的栅格化方案,称为合成:将页面内的各个部分分成多个图层,分别对其进行栅格化,并在合成器线程中单独合成页面。上述操作即采用该方案。
合成器帧通过 IPC 传送给浏览器进程,接着浏览器进程将合成器帧传送到GPU,最终渲染展示到屏幕上。当页面发生变化,如滚动了页面,合成器线程则会生成一个新的合成器帧,再重复上述操作。
综述:
(2) 重排/重绘
重排
当改变一个元素的尺寸位置属性时,会重新进行样式计算、布局(Layout)、绘制(Paint)以及后面的所有流程,这个行为称为重排,也叫作回流(reflow)
重绘
当改变某个元素的颜色属性时,不会重新触发布局(Layout),但还是会触发样式计算和绘制(Paint),这个行为称为重绘(repaint)
重排一定会引起重绘,而重绘不一定会引起重排。
由于重排重绘会占用主线程、同时 JS 也会抢占主线程,这就会导致页面出现卡顿情况。同时,大量的重排重绘会造成额外的计算消耗。所以要尽量减少重排重绘。那么该如何减少重排重绘呢?
- 最少化重排重绘,比如样式集中改变,使用添加新样式类名
.class
或cssText
。 - 使用
absolute
或fixed
使元素脱离文档流,这在制作复杂的动画时对性能的影响比较明显。 - 开启 GPU 加速,利用 css 属性
transform
、will-change
等,因为它们不会触发重排重绘。
6.断开 TCP 连接
当所有操作完,关闭页面,就会断开 TCP 连接。也就是我们常说的四次挥手。
http1.1 是默认不断开 TCP 连接的,因为连接建立需要耗费资源,多个 HTTP 请求会复用 TCP 通道。所以当页面关闭时,TCP 连接才断开。
(1) 四次挥手
首先要明确一点,客户端和服务端都可以发起关闭连接请求。我们假设是客户端发起关闭请求。
FIN 请求(客户端)
主机 A 发送一条
seq=x,FIN=1
的数据包给主机 B,其中seq=x
表示这条数据包的序号。这就是第一次挥手。
ACK 确认(服务端)
主机 B 接收到后根据
FIN=1
知道要断开连接。向 A 发送一条seq=y,ack=x+1,ACK=1
的数据包,其中seq=y
表示这条数据包的序号,ack=x+1
表示这条数据包是对主机 Aseq=x
的数据包的确认。这就是第二次挥手。告诉对方我已经知道了。但是这时候还没有立刻关闭,而是处于一个
关闭等待
的状态。因为这时候服务端可能还在发送数据,只有数据发送完了才能发送 FIN 数据包。FIN、ACK 确认(服务端)
当数据发送完毕,主机 B 发送一条
seq=z,ack=x+1,ACK=1,FIN=1
的数据包给主机 A,其中seq=z
表示这条数据包的序号,ack=x+1
表示这条数据包是对主机 Aseq=x
的数据包的确认。这就是第三次挥手。
ACK 确认(客户端)
主机 A 接收到后,检查
ACK
是否为 1,ack
是否为第一次发送数据包的seq+1
,检查FIN
是否为 1,如果都正确,主机 A 向主机 B 发送一条seq=x+1,ack=z+1,ACK=1
的数据包。主机 B 收到后,检查ack、ACK
是否正确,正确则关闭连接。这就是第四次挥手。
客户端在发送 ACK 数据包后,不是立刻就关闭连接,而是需要等待一段时间,因为虽然理论上四个数据包发送完毕就可以直接关闭连接了,但是网络是不可靠的,可能存在最后这个 ACK 数据包没有被服务端接收到的情况,服务端在长时间没有收到 ACK 数据包后,会重新发送一次 FIN 数据包。如果客户端在发送 ACK 数据包后立刻关闭连接,就无法收到这条重发的 FIN 数据包,导致服务端关闭连接失败,造成资源的浪费。所以客户端在发送 ACK 数据包后,需要等待一段时间,等待时间长度为 2MSL,MSL( Maximum Segment Life )是 TCP 对数据包生存时间的限制,发送后超过这个时间还未被接收到,就认为这个数据包丢失,2MSL 能确保客户端收到重发的 FIN 数据包。
参考:
https://blog.csdn.net/BonJean/article/details/78453547
https://zhuanlan.zhihu.com/p/102149546
https://juejin.cn/post/6935232082482298911
https://juejin.cn/post/6990344840181940261
https://juejin.cn/post/6998389354271866910