[TOC]
前言
可能每一个前端工程师都希望理解浏览器的工作原理,但不知道从何下手,或者从某个角度似乎懂了,但是又不够清晰全面。接下来这几篇文章从点-线-面,从现象到本质,庖丁解牛般来逐步揭开浏览器工作原理的神秘面纱,
- 我们希望知道从在浏览器地址栏中输入 url 到页面展现的短短几秒内浏览器究竟做了什么? (大致通过各个阶段流程,来了解浏览器做了什么?)
1、从输入URL到页面呈现发生了什么?(导航流程阶段)
2、从输入URL到页面呈现发生了什么?(检查缓存阶段)
3、从输入URL到页面呈现发生了什么?(网络请求阶段)
4、从输入URL到页面呈现发生了什么?(解析计算阶段)
5、从输入URL到页面呈现发生了什么?(页面渲染阶段) - 我们希望知道到浏览器的组成,以便更深刻理解整个浏览器各个组成部分及各个进程线程之间如何协调工作(从浏览器自身角度,来理解浏览器工作原理)
- 我们希望了解平时常常听说的各种代码优化方案是究竟为什么能起到优化的作用?(理解了浏览器工作原理,如何突破限制、瓶颈,优化页面,提高页面性能)
URL请求到网页渲染全流程
先看下浏览器从输入地址到页面呈现的大致过程:
1、浏览器处理处理输入信息(判断是搜索内容还是请求URL)
2、浏览器构建请求行信息
3、浏览器查找本地缓存是否存在对应文件(如:index.html
),若缓存命中,请略过中间步骤,直接跳到第9步。若没有,则按照以下步骤进行。
4、浏览器发起真正请求,从url中解析出服务器的主机名,,用于DNS查询,将主机名转换成服务器的IP地址。
5、浏览器从url中解析出端口号,默认80,HTTPS默认端口443。
6、等待TCP队列(浏览器并发请求限制)
7、浏览器建立一条与服务器的TCP/IP连接,三次握手和断开四次挥手。
8、浏览器通过TCP/IP连接向服务器发送HTTP请求,请求数据包。
9、服务器处理HTTP请求,返回响应。
10、浏览器检查HTTP响应是否为一个重定向(3XX结果状态码)、一个验证请求(401)、错误(4XX、5XX)等等,这些都需要根据具体情况分类处理。
11、浏览器接收HTTP响应并且可能关掉TCP连接,或者是重新建立连接使用新请求,获得新响应。
12、浏览器接收响应数据,如果响应头包含可缓存标识则存入缓存。
13、浏览器解析响应信息,并显示HTML页面。
14、浏览器发送请求获取嵌入在HTML中的资源(如CSS、JS、图片、音频、视频等)
15、浏览器发送异步请求。
16、页面全部渲染结束。
[一] 、导航流程阶段
当用户输入信息后,浏览器进程中的UI线程捕捉到输入内容, 如果访问的是网址,则UI线程会启动一个网络线程来请求DNS来进行域名解析,接着通过TCP/IP协议链接服务器获取数据,如果你的输入不是网址而是一串关键词,浏览器认为是去搜索,使用默认搜索引擎来查询。无论那种情况最终目的都是构建请求行,再进入下一个阶段。
GET /index.html HTTP1.1
[二] 、查找缓存阶段
用户输入信息之后,在真正发起网络请求之前,浏览器会先对输入信息进行处理,构建请求行信息,并在浏览器缓存中查找是否有请求的资源。其中,浏览器缓存是一种在本地保存资源副本,以供下次请求时直接使用的技术。当浏览器发现请求的资源已经在浏览器缓存中存有副本,它会拦截请求,返回该资源的副本,并直接结束请求,而不会再去源服务器重新下载。当然,如果缓存查找失败,就会开始进入真正的网络请求过程了。
那浏览器的缓存机制是什么?
浏览器缓存机制是指通过 HTTP 协议头里的 Cache-Control(或 Expires)和 Etag(或 Last-Modified)等字段来控制文件缓存的机制。浏览器与服务器通信的方式为应答模式,即是:浏览器发起HTTP请求 – 服务器响应该请求。那么浏览器第一次向服务器发起该请求后拿到请求结果,会根据响应报文中HTTP头的缓存相关标识,决定是否缓存结果,是则将请求结果和缓存标识存入浏览器缓存中。
了解完浏览器的缓存机制,再来看下具体如何实现:
浏览器的缓存机制是通过这两个缓存阶段实现的,也可以理解成两种策略:新鲜度
和校验值
来确认本地缓存副本的有效性。
缓存方式 | 获取资源形式 | 状态码 | 发送请求到服务器 |
---|---|---|---|
强缓存阶段 | 从缓存取 | 200(from cache) | 否,直接从缓存取 |
协商缓存阶段 | 从缓存取 | 304(not modified) | 是,正如其名,通过服务器来告知缓存是否有效 |
再回到问题本身,查找缓存阶段这个过程中发生了什么?
输入地址之后请求行构建之后,在真正发起网络请求之前,浏览器会根据请求行信息匹配缓存。
1、进入强缓存阶段(查找本地缓存,检查新鲜度)浏览器发送请求前会根据请求头的expires和cache-control判断是否命中强缓存策略,如果命中,直接从缓存获取资源,不发送请求,如果没有命中,则进入下一步
2. 如果浏览器确认已过期(强缓存失效),则进入协商缓存阶段(校验文件是否被更改),发送真正http请求(携带上次请求返回了资源缓存标识Last-Modified/Etag
(如果有返回),服务器根据相对应请求头字段(If-Modified-since/If-None-Match
)来确认资源文件是否更新,从而判断返回304还是200来告知浏览器本地缓存副本的有效性),从而判断返回304还是200来告知浏览器本地缓存副本的有效性)。
当然并不是所有的http请求都会经历强缓存和协商缓存阶段,这个取决与服务器所使用的缓存策略。
[三] 、网络请求阶段
大致过程如下:
- 查询DNS(网络)
- 等待TCP队列(本地)
- 建立TCP连接(网络)
- 发送请求(网络)
- 等待响应(网络)
- 接收数据(网络)
- 断开连接(本地)
3.1 DNS查询
这个过程中根据构建的请求行信息中获取资源URL,然后从URL中抽取出域名字段,进行DNS域名解析。这之前首先了解下DNS
DNS(Domain Name System),域名服务系统的功能是将服务器名称和 IP 地址进行关联 。互联网中设备是通过IP地址来查找的,而通常我们输入地址URL,作为URI(一个用于标识某一互联网资源名称的字符串)的一种形式,是通过协议、域名或IP地址和资源目录位置组成的,因此需要通过访问DNS服务器才能得知IP地址,从而将消息发送给对应的目标。
DNS解析过程如下: 1、查找浏览器缓存:浏览器会缓存2-30分钟访问过网站的DNS信息 - - 地址栏输入chrome://net-internals/#dns 查看 2、检查系统缓存:检查hosts文件,它保存了一些访问过网站的域名和IP的数据 - windows平台 ipconfig /flushdns 来清空dns 缓存内容。你也可以用命令 ipconfig /displaydns 来查看dns 缓存内容。 3、检查路由器缓存:路由器有自己的DNS缓存,如未找到 4、检查ISP DNS缓存:ISP服务商DNS缓存(本地服务器缓存),如未找到 5、递归查询:从根域名服务器到顶级域名服务器再到极限域名服务器依次搜索对应目标域名的IP -
3.2 等待TCP队列
随着现在的网页设计的越来越炫酷,功能越来越丰富。伴随着的是网页加载的资源越来越多,常常一个页面加载的CSS、JS、图片、接口等超过几十上百个。对客户端操作系统而言,过多的并发涉及到端口数量和线程切换开销。对服务器而言,若将所有请求一起发给服务器,也很可能会引发服务器的并发阈值控制而被BAN。
基于多方面因素考量出的优化结果:HTTP/1.1有Keep Alive,支持复用现有连接,等请求返回回来后,再复用连接请求可以快很多。因此,浏览器并不一定会对每个资源开个TCP连接去请求加载,浏览器对同一时间内请求数做了并发限制。所以当请求一个功能复杂的页面,解析过程中发现需要资源较多情况下,会触碰到浏览器最大并发数,后续资源资源只能等待当前资源加载完成,释放TCP连接,交给后续请求复用。
3.3 通过三次握手,建立TCP连接
如果是HTTPS,HTTPS其实由两部分组成:HTTP + SSL/TLS,也就是HTTP 基础上加了一层处理加密信息的模块,服务端和客户端之间传输的信息都会经过TLS加密
1、第一次握手:建立连接,客户端发送请求连接报文段,将SYN位置为1,Sequence Number为x,然后客户端进入SYN_SEND 状态,等待服务器确认。
2、第二次握手:服务器端收到SYN报文段。服务器端需要对收到的SYN 报文段进行确认,设置Acknowledgement Number 为x+1(即Sequence Number +1),同时自己还要发送SYN请求信息到客户端,将SYN位置为1,Sequence Number 为y,服务器将上述两个信息放到同一个报文段(即SYN + ACK报文段),一并发送给客户端,此时服务器端进入SYN_RECV状态。
3、第三次握手:客户端收到服务器端的(SYN + ACK 报文段),然后将Acknowledgement Number 设为y+1,向服务器端发送ACK报文段,这个报文段发送完毕后,客户端和服务器端都进入ESTABLISHED状态,完成TCP三次握手。
3.4 一段时间后,通过四次挥手,断开TCP连接
当HTTP 请求到达服务器,服务器进行对应的处理。最后要把数据传给浏览器,也就是返回网络响应。浏览器这时候要判断Connection
字段, 如果请求头或响应头中包含Connection: Keep-Alive,表示建立了持久连接,这样TCP
连接会一直保持,之后请求统一站点的资源会复用这个连接。否则断开TCP
连接, 请求-响应流程结束。
[四] 解析计算阶段
输入URL后,浏览器通过网络模块发起的HTTP请求的资源返回之后,如果响应头中Content-Type
的值是text/html
,那么接下来就是浏览器的解析
和渲染
工作了。准确地说,浏览器需要加载解析的不仅仅是HTML,还包括CSS、JS,以及还要加载图片、视频等其他媒体资源。浏览器通过解析HTML,生成DOM树(如在解析过程中有存在 Javascript 代码就交给 Javascript 引擎处理,处理完成返回给 DOM 树),解析CSS,生成CSS规则树,然后通过DOM树和CSS规则树生成渲染树。渲染树与DOM树不同,渲染树中并没有head、display为none等不必显示的节点。要注意的是,浏览器的解析过程并非是串连进行的,比如在解析CSS的同时,可以继续加载解析HTML,但在解析执行JS脚本时,会停止解析后续HTML,这就会出现阻塞问题,关于JS阻塞相关问题,这里不过多阐述,后面会单独开篇讲解。
大致过程如下
- HTML解析,生成DOM Tree
- 解析CSS,生成CSSOM
- 结合DOM和CSSOM生成Render Tree 4.遍历Render Tree 生成Layout Tree
4.1 解析HTML, 生成 DOM Tree
DOM Tree 定义
:由于浏览器无法直接理解HTML字符串
,因此将这一系列的字节流转换为一种有意义并且方便操作的数据结构,这种数据结构就是DOM Tree
。DOM Tree
本质上是一个以document
为根节点的多叉树。
DOM Tree构建构建过程
1、转换Conversion。浏览器从磁盘或网络读取HTML的原始字节(0和1组成的字节数据),并根据文件的指定编码(例如 UTF-8)将它们转换成字符串。
2、分词Tokeniser。 将字符串转换成Token,例如:<html>
、<body>
等。Token中会标识出当前Token是“开始标签”或是“结束标签”亦或是“文本”等信息。
3、词法分析Lexing。将分词得到的一堆token转换为节点对象,这些对象分别定义它们的属性和规则。
4、DOM构建。利用不同节点对象Node之间的关系,构建 DOMTree
注意
:整个构建过程并不是等Token 完全转换完成才去生成节点构建DOM Tree,而是一边生成一边消耗,这个很容易解释,token 生成过程中可能遇到<link>、<img>、<script>
,因此解析过程中的需要加载的每个资源文件都是重复http请求过程(缓存检查阶段+网络请求阶段)
4.2 加载并解析CSS,生成CSSOM
浏览器解析CSS代码包括四部分:浏览器默认基本样式、外部的(link
),导入的(@import
)、内联的(如:style="display:none"
)。如果遇到 link 标记,浏览器就会立即发送请求获取样式文件。当然我们也可以直接使用内联样式或嵌入样式,来减少请求;但是会失去模块化和可维护性。
在计算 css 规则的时候,我们会在已经构建好的元素上,去检查它匹配到了哪些规则,再根据规则的优先级,做覆盖和调整。并且 CSSOM 主要是DOM 结构上的盒的描述,他基本上是依附于 DOM 树的。CSS 计算是把 CSS 规则应用到 DOM 树上,为 DOM 结构添加显示相关属性过程。
CSSOM 是有 rule 部分
和 view 部分
的,rule 部分
是在 dom 开始之前就构件完成的,而 view 部分
是跟着 dom 同步构建的
需要注意的是解析CSS规则树,JS将暂停,直至css规则树就绪。
- JavaScript 不仅可以读取和修改 DOM 属性,还可以读取和修改 CSSOM 属性。因此,存在阻塞的 CSS 资源时,浏览器会延迟 JavaScript 的执行和 DOM 构建
- CSS 解析会阻塞渲染流程,这意味着浏览器将不会渲染任何已处理的内容,直至 CSSOM 构建完毕。
4.3 构建Render Tree
渲染树和 DOM 元素相对应的,但并非一一对应。这个过程中,非可视化的 DOM 元素不会插入呈现树中,例如“head”元素。同时从DOM Tree 中剔除display:none的节点,以及创建一些不存在原有dom结构树中的元素,伪元素如::before
、::after
、::first-letter
,这些被添加的内容会以具体的UI显示出来,被用户所看到的,这些内容不会改变文档的内容,不会出现在DOM中,不可复制,仅仅是在CSS渲染层加入。
4.4 生成布局树Layout Tree
通过遍历渲染树中渲染对象的信息,计算出每一个渲染对象的位置和尺寸,生成Layout tree
布局树生成的大致工作如下:
- 遍历生成的 DOM 树节点(不包含display为none的节点),并把他们添加到
布局树中
。 - 计算布局树节点的坐标位置。
根据渲染树布局,计算CSS样式,即每个节点在页面中的大小和位置等几何信息。HTML默认是流式布局的,但CSS和js会打破这种布局,改变DOM的外观样式以及大小和位置。这时就要提到两个重要概念:repaint和reflow。
repaint
:屏幕的一部分重画,不影响整体布局,比如某个CSS的背景色变了,但元素的几何尺寸和位置不变。reflow
: 意味着元件的几何尺寸变了,我们需要重新验证并计算渲染树。是渲染树的一部分或全部发生了变化。这就是Reflow,或是Layout。
[五] 页面渲染阶段
5.1 生成Layer Tree
浏览器在构建完布局树
之后,还会对特定的节点进行分层,构建一棵图层树
(Layer Tree
)。在显示到屏幕之前首先得确定绘制顺序。因此为了确定绘制顺序,主线程遍历Layout Tree,对它进行分层并创建一个绘制记录表,该表记录了绘制的顺序,生成Layer Tree
5.2 栅格化(Rastering )
什么是栅格化呢?
- 将要绘制的信息转化为像素点,显示到屏幕上
什么是合成呢?
-
将页面的多个部分分成多个图层,分别对其进行栅格化并在合成器线程中单独进行合成页面。整个过程如下:
-
- 当Layout tree 生成完毕 和绘制顺序确定后,主线程将这些信息传递给合成器线程,合成器线程将每个图层栅格化
-
- 由于一个图层可能跟页面一样大,合成器线程将它们切分为许多图块,然后发给栅格线程
-
- 栅格线程栅格每个图块,并存储到GPU内存中
-
- 栅格化完成后,合成器线程根据这些图块生成合成器帧,通过IPC传递给浏览器进程
-
- 浏览器进程将合成器帧交给GPU,然后GPU渲染到屏幕上
结语
本文从头到尾都在回答一个问题,但是这个问题涉及到知识点太多,不方便一一展开讨论。
Q: 从输入URL到页面呈现发生了什么? A: 整个过程大致划分为5个阶段
- 导航流程阶段
- 检查缓存阶段
- 网络请求阶段
- 解析计算阶段
- 页面渲染阶段
由于篇幅原因,还有两个问题接下来将展开讨论
- 我们希望知道到浏览器的组成,以便更深刻理解整个浏览器各个组成部分及各个进程线程之间如何协调工作
- 我们希望了解平时常常听说的各种代码优化方案是究竟为什么能起到优化的作用?