浏览器的原理
我们在浏览器中输入一个域名或者IP,总之是一个网络地址。随后便可以完成各种功能,可以看电视、玩游戏、画图、甚至是写代码。浏览器是怎么工作的呢?
浏览器执行步骤
大概的步骤如下:
- 浏览器从
DNS
服务器中进行域名查询,浏览器解析域名拿到对应的IP
地址 - 通过
IP
地址建立TCP
请求连接(三次握手) - 浏览器向服务器发送
http
请求包,服务器请求处理响应 - 服务器返回
HTTP Response
后,浏览器开始接收数据,进行资源下载,解析,页面渲染、js
代码执行等
DNS服务是为了把域名转成IP,域名只是为了方便人类记忆,如果直接用IP访问无需DNS服务器,并且速度会更快。
步骤解析
1.DNS Lookup
DNS是一个分布式数据库,用于维护URL到其IP地址的映射关系,DNS Lookup
是浏览器从DNS服务器中进行域名查询的过程,在一次请求中,首先会进行页面本身的域名查询,在浏览器在解析HTML
代码过程中,需要加载JS
,CCS
,Image
等资源的时候也需要进行域名解析。
域名查询的时候首先会查找DNS缓存,DNS缓存包括浏览器缓存,操作系统本地缓存,路由器缓存以及ISP(本地通信服务商)缓存,如果不存在缓存,这时候就会发起DNS查询
DNS是分布式域名服务器,因此当本地DNS(ISP分配给用户的DNS服务器,具有缓存的能力,对应ISP缓存)没办法提供正确的IP地址的时候,就需要在互联网上搜索多个DNS服务器(包括根域名服务器,顶级域名服务器,权威域名服务器),以此来找到正确的IP地址。
DNS查找一般有三种类型(递归,迭代,非递归),优化的DNS解析过程可以缩短传输距离,DNS查询报文会经过许多路由器和设备才会到达响应的服务器,经过每个服务器的时候都会使用路由表来确定最优的路由路径。
在DNS查询阶段可进行以下优化:
- DNS缓存优化
- DNS预加载策略(DNS Prefetching 是让具有此属性的域名不需要用户点击链接就在后台解析,而域名解析和内容载入是串行的网络操作,所以这个方式能 减少用户的等待时间,提升用户体验)
- 页面中资源的域名合理分配
- 稳定可靠的DNS服务器等
比如在阿里云购买域名后免费提供的DNS服务就是普通的DNS服务,延迟相对较高解析速度慢因此有付费DNS服务。
2.ARP请求
DNS查询获得IP地址之后,还需要进行ARP(地址解析协议)请求,从而获得对应的MAC地址,之后才能完成通信,ARP以IP地址为线索,定位下一个应该接收数据分包的主机MAC地址。
ARP会维护每个主机和路由器上的APR缓存(或表),缓存中维护着的是每个IP到MAC地址的映射关系,当一个映射关系被缓存过后,下次再向这个地址发送数据时就不需要再次发起ARP请求了,ARP请求通常使用UDP协议(因为需要广播)。
为什么有了IP地址还要加上MAC地址呢?
首先TCP/IP
是网络层的协议,根据网络的不同同一台机器的ip地址也会发生改变,因此并不能通过IP地址实现与计算机的一一映射,而MAC地址在链路层,与计算机是一一绑定的(出厂的时候就焊死了,是唯一的标识),因此能通过MAC地址实现与计算机的一一映射。其次,仅通过IP地址必须通过在局域网内广播才能与主机匹配,如果只采用IP定位,就会造成网络上大量的广播包,形成广播风暴,大大浪费网络带宽和资源。若采用IP和MAC地址则能确定主机位置,只需要第一次通信广播将IP和MAC相互映射,之后都是通过单播通信。
3.建立TCP请求
浏览器获取到目标地址的IP地址之后,就能获取到对应的端口号(http
默认80
,https
默认443
),接着进行对应的数据包封装,包封装好之后会进行TCP请求,这就是很熟悉的TCP/IP
三次握手,三次握手的目的是同步连接双方的序列号和确认号并交换 TCP 窗口大小信息。
简单的描述一下三次握手如下:
- 服务端进程准备好接收来自外部的TCP连接,等待客户端发送请求
- 客户端打开连接,发送信息给服务器端
- 服务器端接收到信息,将接收到信息这个信息回传给客户端,并且为这次通信分配资源
- 客户端收到回传信息,说明通道已经联通了,就将这个信息回传给服务端,并分配资源 开始进行通信
4.TCP传输
建立好连接之后,就开始进行TCP传输,在服务器端,可以通过优化来降低响应响应给客户端的网络耗时。TCP传输是分段的,一个HTTP响应报文会被操作系统切成多个MSS大小(一般为1460B
),发送端每次只会按顺序发送若干段,通过拥塞窗口和接收端窗口,知道接收端接收到完整的报文为止。报文越大,受拥塞控制算法的影响也越大。
接收端流量控制用的是滑动窗口机制,本质上是描述接收方的TCP数据报缓冲区大小的数据,通过这个窗口大小,TCP可以慢慢从数据的左边移到数据的左边,从而实现按顺序进行分段数据的发送。(知道一次能接收多少所以这边才能一次传多少过去)
发送端流量控制用的是拥塞窗口机制,拥塞窗口的原理:TCP发送方先发送少量的数据报文段,然后等待对方的回应,ACK回应后就把这个窗口的大小加倍,然后连续发送两个数据包,对方回应后,继续加倍,知道发生错误或丢包,这样就能检测到网络的承载能力,从而按这个大小发送数据,这个阶段的优化可以通过减小HTTP报文大小和HTTP头的大小来实现,因为报文越小,传输所需要的RTT次数越少,耗时越短(Ajax
异步化可以减少这个问题)
在传输阶段可进行以下优化:
- 基于不同网络环境,优化数据包的大小,以减少数据因传输丢失或者被破坏产生的重传,从而提高传输效率
- 对网络传输链路进行优化(通常需要很大的投入)
- TCP连接关闭的四次挥手
- 传输结束之后要记得关闭TCP连接
四次挥手的简单过程:
客户端发送给服务器端FIN包(告诉它要关闭了)
服务器端告诉客户端已经收到准备关闭的消息(这时候可能还有数据没处理完,服务端会继续处理数据)
服务器告诉客户端信息已处理完
客户端收到回传信息,关闭通信
5.浏览器向服务器发送请求包
浏览器会向服务器发起请求,在HTTP
中,请求方法包括简单请求和非简单请求:
- 简单请求:不能触发
CORS
预检请求,Content-Type
的值仅限于下列三者之一:text/plain
,multipart/form-data
,application/x-www-form-urlencoded
。包括GET请求(参数从url
中带走)以及POST
请求(参数通过body
带走) - 非简单请求:先发送预请求
OPTIONS
,预请求成功之后才会发送真实请求,OPTIONS
的请求是由Web
服务器处理跨域访问引发的,包括PUT
请求(一般用于文件上传),DELETE
请求(删除操作)等
除请求内容之外,HTTP
请求还包括很多信息,这些信息都被放在头部,一起带给服务器。HTTP
是基于TCP
协议的,除了HTTP
请求之外,根据业务需求,还会有ws
请求,ftp
请求等。
6.服务器处理请求并返回响应
这个过程主要工作是服务器端处理数据,返回数据,主要的性能优化点在降低服务端RT(Response Time)
上。服务器端返回的响应包含请求的网页以及状态码,压缩类型,如何缓存页面,还有需要设置的cookie信息,隐私信息等。
服务端请求处理响应主要包括以下几个步骤:
- Web服务器根据请求类型,将请求转发给已经注册该请求的应用服务器来处理
- 应用服务器接收请求,分配给对应的代码单元处理
- 从缓存,数据库,文件系统等获取数据
- 基于业务进行数据逻辑处理
- 基于模板进行数据格式化渲染
- 应用服务器将格式化渲染的数据返回Web服务器
- Web服务器将最终响应内容经过Gzip压缩后,返回客户端(可不启用Gzip)
在服务器端,每一步都有可优化的地方,对中小型网站而言,重心可能会放在数据获取过程的优化上,对大型网站而言,每一步的优化都可能带来质的提升。
7.浏览器进行资源下载,解析,渲染
最后一步是浏览器的渲染,大体的渲染过程如下:
- 浏览器解析
HTTP
的头部代码(如果是Gzip
那会先解压),下载头部中引用的css
文件或js
文件 - 解析
html
代码和css
代码,解析出DOM
树和CSSDOM
树 - 通过上面两棵树构造
Rendering Tree
- 根据
Rendering Tree
完成绘制过程
浏览器
整个渲染过程基本都在浏览器中进行,所以我们需要先了解浏览器的相关知识,浏览器一般由以下几部分构成:
- 用户界面:除了主窗口渲染界面之外的其他显示部分
- 浏览器引擎:主控用户界面和渲染引擎,为它们之间传送指令
- 渲染引擎:负责显示请求的内容(解析html和css等)
- 网络模块:处理网络请求,同时还提供获得的文档的缓存,为所有的平台提供底层实现
- js解释器:解释和执行js代码(著名的v8引擎)
- 用户界面后端:绘制基本的窗口小部件,比如组合框和窗口,在底层使用的是操作系统的用户界面方法,并且公开通用的接口
- 数据存储:管理用户数据,比如书签,偏好设置等
浏览器线程和进程
进程可以被描述为是一个应用的执行程序,线程存在于进程并执行程序任意部分。如果两个进程需要进行对话,可以通过进程间通信(IPC)来进行
浏览器是单进程还是多进程?
浏览器可能是一个拥有很多线程的进程,也可能是一些通过IPC通信的不同线程的进程,Chrome浏览器
属于多渲染进程架构。Chrome
进程主要包括浏览器进程,渲染进程,插件进程和GPU进程四个部分:
- 浏览器进程:控制应用中的
Chrome
部分,包括地址栏,书签,回退和前进按钮等,还处理一些不可见的操作例如网络请求与文件访问等。 - 渲染进程:控制标签页内的网站展示,简单来说就是每个标签都有自己的渲染进程,这样就算其中一个标签失去响应,也不会影响其他标签的正常使用
- 插件进程:控制站点使用的任意插件
- GPU进程:处理独立于其他进程的GPU任务。因为GPU会被分成不同进程,处理来自不同应用的请求并绘制在相同表面
- 网络进程:主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里,并没有强制的规范,不一定是一个单独的进程
多进程的好处:
安全性和沙箱化:由于操作系统提供了限制进程权限的方法,浏览器就可以用沙箱保护某些特定功能的进程沙箱看成是操作系统给进程上了一把锁,沙箱里面的程序可以运行,但是不能在你的硬盘上写入任何数据,也不能在敏感位置读取任何数据
多进程的缺点:
进程有自己的私有内存空间,所以一些公共部分只能被拷贝不能像线程之间一样共享,所以就会消耗更多内存
浏览器一直都在致力于如何提供更好的用户体验,明白浏览器如何处理开发者写的代码,能够反过来为用户提供更多友好体验。
从浏览器内部处理逻辑的角度看浏览器是怎么工作的;
- 处理输入:UI线程会询问输入的是
url
地址还是一个查询条件(Chrome
地址栏也可以作为搜索栏操作),此时UI线程需要解析和决定把请求发送到搜索引擎或者要请求的网站中去。 - 开始导航:UI线程通知网络进程调取去获取站点内容,网络进程开始工作,比如建立连接,下载资源等
- 读取响应:数据返回时网络进程需要判断响应报文,如果响应是个
HTML
文件,下一步就会将数据传给渲染进程,如果是一个压缩文件或者其他文件,就需要将数据传递给下载管理器,这个时候也会进行安全检查和CORB(cross origin read blocking)
检查,以保证敏感的跨域数据不被传给渲染进程 - 查找渲染进程:一旦检查执行完毕而且网络线程确信浏览器能够导航到请求的站点,网络线程会告诉UI线程所有的数据准备完成,UI线程会寻找渲染进程,开始渲染Web页面,每个标签对应一个渲染进程。但如果从一个页面打开了另一个新页面,而新页面和当前页面属于同一站点的话,那么新页面会复用父页面的渲染进程
- 提交导航:数据和渲染进程都准备就绪时,浏览器进程会发送一个IPC到渲染进程去提交导航。它也会传递数据流,所以渲染进程可以保持接收HTML数据,一旦浏览器进程收到渲染进程已经提交的确认信息,导航完毕并且文档加载解析开始
额外的步骤:初始加载完毕:一旦导航被提交,渲染进程开始加载资源和渲染页面,一旦渲染进程渲染完毕,会发送一个IPC返回给浏览器进程,此时UI线程停止标签页上的加载动画,但是客户端js可以在此时仍然加载额外资源并重新渲染视图
浏览器渲染过程
不同内核的浏览器渲染过程可能存在细微的区别,但大致的流程是一致的,在此以webkit
为例进行分析
解析阶段
渲染进程的核心是将HTML
,CSS
和Javascript
转换为用户可以与之交互的网页,渲染进程内部包括主线程,工作线程,合成线程和光栅线程。
在实际应用中,解析阶段主要是主线程在工作。渲染引擎会先解析HTML
头部代码,下载样式表,下载的同时HTML
会继续解析,等到解析完成之后,构造生成DOM
树,开始解析下载好的CSS
,构造CSSOM
。
这个时候主线程解析CSS
并确定每个DOM
节点计算后的样式(没下载完不能开始构建)。样式计算的目的是为了计算DOM
节点中的每个元素的具体样式。
大体分为三个步骤:
-
把
CSS
转换为浏览器能够理解的结构(渲染引擎接收到CSS
文本时,会执行转换操作,将CSS
文本转换为styleSheets
) -
转换样式表中的属性值,使其标准化(将不容易被渲染引擎理解的属性标准化,
2em->32px
这种) -
计算出
DOM
树中每个节点的具体样式,等两棵树都解析完了(两棵树是可以并行解析的),就开始进行渲染树的构造。为了加快速度,预加载扫描器(preload scanner
)会在主线程处理外部资源请求时同时运行,当HTML文档中有和之类的内容,预加载扫描器会查看由HTML
解析器生成的标记,并在浏览器进程中向网络线程发送请求。如果当中遇到JS
代码,DOM
的解析就会被迫停止,因为浏览器认为JS
代码可能会修改DOM
结构,所以需要等到JS
执行完毕之后,DOM
才会接着解析
还会出现一种特殊情况,就是当CSSOM
还没下载完成的时候,HTML
解析时遇到JS
代码,这个时候浏览器会暂停脚本执行,直到CSS
下载完,并完成CSSOM
构建再继续执行
布局阶段
布局是个递归计算元素几何形状的过程,它会从根节点开始,遍历部分或者所有的框架层次结构,界面首次渲染的时候肯定会全部遍历,但是当发生一些细小变更时,全部进行重新排布就会消耗性能,因此浏览器采取了一种dirty位
系统,如果某个节点发生了变更,就把它以及它的子孙们都标记成dirty
,将标记的这部分进行重新布局(重排),还会进行重新的绘制,标记有两种:dirty
和children are dirty
。children are dirty
表示尽管呈现器自身没有变化,但它至少有一个子代需要布局。
重排一般是异步触发,但是请求样式信息(例如offsetHeight
)或者操作DOM
的脚本会导致重排的同步触发,因为如果操作了DOM
,后面的JS
又不能获取最新的DOM
,就会出现Bug。但是这个特性也会因此带来一些阻塞,因为DOM
没渲染完成之前JS
就不能继续。
重排的具体触发条件:
DOM
树结构的变化(DOM
的添加,删除等)
请求样式信息:比如offsetTop
、offsetLeft
、 offsetWidth
、offsetHeight
、scrollTop
、scrollLeft
、scrollWidth
、scrollHeight
、clientTop
、clientLeft
等,可以在获取时做适当的缓存来减少重排
未脱离文档流的情况下改变了元素的位置
DOM
元素的几何属性变化(宽高,margin
之类的)
元素内容的改变
元素的缩放,旋转,或者给元素添加动画
反正就是元素位置会变的时候,和原来布局不一样了,所以要重新布局
绘制阶段
绘制阶段,主线程会遍历呈现树,并调用渲染引擎的paint
方法,将内容显示在屏幕上,绘制时元素会进入堆栈样式上下文,然后从后往前绘制,绘制的顺序如下(进入堆栈的顺序和这个刚好相反):
- 轮廓
- 子代
- 边框
- 背景图片
- 背景颜色
一般来说,当DOM
修改只是导致样式变化,并没有影响几何属性,节点位置不会发生改变的时候,渲染引擎会跳过布局阶段,直接进入绘制阶段,对元素进行重新绘制(重绘),也就是说一些简单的样式变化,就会触发重绘,包括但不仅限于以下几种情况:
- 背景颜色,字体颜色的改变
visibility
和opacity
这种不会改变宽高,大小的样式
页面合成
浏览器已经知道文档结构,每个元素的样式,页面的几何形状和绘制顺序后,下一步就是将这些信息转换为屏幕上的像素,这个过程称之为光栅化。最原始处理这种情况的方法是,先在光栅化视窗内的画面,如果用户滚动页面,则移动光栅框,并光栅化填充缺少的部分。但是,现代浏览器会运行一种被称之为“合成”的方式对信息进行光栅化。
合成是一种将页面各部分分层,分别光栅化,并在合成线程中合成为页面的技术
如果此时发生滚动,由于图层已经光栅化,要做的只是合成一个新帧,动画也可以用相同的方式(移动图层和合成新帧)实现。
主线程为了区分哪些元素位于哪些图层,会遍历布局树创建图层树,当图层树被创建并且确定绘制顺序之后主线程就会将该信息提交给合成线程,进行每个图层的光栅化。当图层过大时,合成线程还会将它们分块后发送给光栅线程,由光栅线程光栅化每个小块后存储在显存中。
合成线程会给不同的光栅线程设置优先级,同时还具有多个不同分辨率的快,可以处理放大操作等动作,一旦块被光栅化,合成线程就会收集这些块的信息(这个过程称为绘制四边形)创建合成帧(一个绘制四边形的集合,代表一个页面的一帧)
接着,合成帧通过IPC提交给浏览器进程,同时可通过 UI 线程或其他插件的渲染进程中添加另一个合成帧。将这些合成器帧都发送到发送到 GPU ,显示到屏幕上。如果接收到滚动事件,合成线程会创建另一个合成帧发送到 GPU
合成的好处就是它可以在不涉及主线程的情况下完成
这也就是为什么
CSS
动画会比JS
性能好的原因
总结
浏览器工作原理是一个非常复杂的过程,可以说就是Web应用的操作系统。简单的了解浏览器的工作原理对于网站的优化还是有非常大的帮助的。
但是对于个人网站低成本且效果明显的优化手段我推荐:
- CDN技术
- 使用图床/对象存储等替代本地存储
对于我们的小网站来说计算需求很少,网站体验差的主要原因在于加载比较慢。一般都是因为我们的服务器带宽很低导致的(服务器出口带宽极其昂贵),我见过很多使用WordPress
搭建的站点图片存储在服务器本地的。加载图片等静态资源十分占用带宽导致网站打开很慢。
首先我们可以把图片存在对象存储上比如阿里云的OSS,默认情况下OSS的访问速度就已经很快了,但是一般我们使用廉价的单地域OSS,比如你的OSS在杭州,那么你在东北或者海南访问还是有些慢的。这个时候可以引入CDN技术。CDN可以将你的静态资源缓存在当地的缓存服务器,因此在访问的时候优先查CDN的数据。
另外对象存储和CDN这些服务其实很多平台都有提供每月免费额度10-100G
不等 完全够个人使用,即便使用付费的阿里云CDN也是非常便宜。