浏览器解读资源
前言
其实这个话题也是前端面试中另外的一个面试点,即从浏览器输入URL到页面加载完成的过程分析。在开始下面的正式总结之前,先列出几点我对这此新的认知点。
- 服务器会存储许多的静态资源,再拿到服务器的IP地址之后,会返回index.html文件,其实了解前端工程化打包的都知道,一个项目在经过webpack打包之后,生成的dist文件夹下的资源入口就是index.html,在浏览器拿到这个html文件之后,会进行解析。遇到被link的css文件之后会到服务器下载对应的css文件;遇到script标签的js文件回到服务器下载对应的script文件。
- 不同的浏览器有不同的内核,由早期的Gecko、到现如今的Webkit和Blink。
- 浏览器进行资源解析时,会用到浏览器引擎进行资源解析,不同浏览器的解析引擎是不一样的。浏览器引擎其实也可以看作是浏览器的排版引擎(layout engine),或者叫作页面渲染引擎(render engine)或者样板引擎等。无论被叫做什么,浏览器引擎都是浏览器对资源解析最重要的一环。
输入URL时发生的事
1、输入URL之后,首先进行的第一步就是对浏览器的URL进行解析,解析的时候其实也会有许多注意项。
- 为什么URL需要解析呢?
URL中可能存在一些特殊字符需要进行转义,如果不转义的话,可能导致传输的数据发生错误。常见的特殊符号有
@ & = , / ? - _ ~ !
- URL的编码规则是什么? utf-8
- 是不是所有的浏览器的URL编码都是utf-8? 应该大多数吧,我看过chrome和百度的搜索引擎都是这样的
- 如何保证URL的解析都是utf-8编码? 可以使用encodeURLComponent
- encodeURLComponent和 encodeURL有啥区别?
encodeURlComponent的编码范围更广,即对保留字符
;,/?:@&=+$
,也会进行编码,但是encodeURL不会。
2、DNS解析流程
1.例如URL输入https://www.baidu.com,会取出这个地址对域名,即`www.baidu.com`,首先浏览器会检查自身的缓存中是否存在这个域名解析后对应对ip地址,有则进行返回,解析结束。如果还没命中,则会进入第二点。
2.第二点主要是对用户的系统的host文件中是否存在此域名对应的ip地址,有则返回,解析结束。否则会进入 第三点。
3.进入第三点,就会真正的请求本地的域名服务器(LDNS)进行查询,(LDNS,按照其他资料查询所得,应该是位于用户本地城市的一台域名服务器,这里会缓存许多域名对应的IP地址)一般到此的话域名解析应该都会成功命中了。如果还没有的话,会进入下面的查找。
4.根域名服务器返回给LDNS一个所查询域的主域名服务器(gTLD Server,国际顶尖域名服务器,如 .com .cn .org等)地址。
5.此时LDNS再发送请求给上一步返回的gTLD。
6.接受请求的gTLD查找并返回这个域名对应的Name Server的地址,这个Name Server就是网站注册的域名服务器。
7. Name Server根据映射关系表找到目标ip,返回给LDNS。
8.LDNS缓存这个域名和对应的ip。
9.LDNS把解析的结果返回给用户,用户根据TTL值缓存到本地系统缓存中,域名解析过程至此结束。
附:从第4点到第8点,其实也是CDN的做法。
3、查找到IP之后
查找到IP之后,就是建议与此IP的服务器之间的连接,这里就会涉及到http协议的三次握手和四次挥手。在详细阐述建立连接过程之前,我们先来了解一下位码。
位码是tcp的标志位,有6种标识。
- SYN(synchronous建立联机)
- ACK(acknowledgement 确认)
- PSH(push传送)
- FIN(finish结束)
- RST(reset重置)
- URG(urgent紧急)
三次握手
- 第一次握手:建立连接的时候,客户端发送
syn包(syn=j)
到服务器,客户端进入SYN_SEND状态,等待服务器确认。 - 第二次握手:服务器收到syn包,必须确认客户端到SYN(ack=j+1),同时自己也发送了一个syn包(syn=k),即SYN+ACK包,此时服务器进入到SYN_RECV状态。
- 第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手,客户端与服务器建立连接,开始传送数据.
为什么需要三次握手而不是二次握手? 其最主要的就是保证双方建立稳定的连接传输通道;如客户端因网络原因断开连接了,那么server在如果不经过第二次握手,就会一直等待客户端发来的请求,这样就会导致资源浪费。
四次挥手
- 第一次挥手: 客户端送一个带有FIN(结束)的结束码的询问信息。
- 第二次挥手: 服务端返回一个ack确认收到的信息给客户端。
- 第三次挥手 服务端返回一个FIN(结束)的结束码给客户端。
- 第四次挥手 客户端发送一个ack确认收到的信息给服务端,结束这次连接。
4、缓存index.html文件
建立完成连接之后,就会开始请求index.html文件(一般来说首页都是index.html),这个时候就会看浏览器缓存中是否存在对应的文件,如果存在则直接返回,如果没有就会去服务器中获取。
这其中缓存也是特别重要的一环,常了解到的就是强缓存和协商缓存。因为在页面进行二次加载时,首先要经过强缓存的处理,cache-control
是判断是否进入强缓存,如果为no-cache,则进入协商缓存。还有就是如果cache-controle: max-age=xxx;则会将本次请求和上次的成功请求返回的时间差和这个max-age进行比对,没有超过,则命中强缓存,直接从本地文件中读取返回;如果超过了则进入协商缓存。协商缓存主要是在请求资源时,如果向服务器发送header带有If-None-Match和If-Modified-Since的请求,则服务器会比较Etag。
ETag比对:
- 如果资源没有被更新,则返回304状态码,浏览器读取本地缓存。
- 如果资源更新,返回200状态码,并且返回的文件中带上新的ETag。
If-Modified-Since比对:
- 主要是If-Modified-Since的值跟服务器获取到此文件最近改动时间进行比对,如果一致则命中协商缓存。返回304。
- 如果不一致,则返回新的last-modified文件并返回200.
Etag是URL的Entity Tag,用于标示URL对象是否改变,区分不同语言和 Session等等。具体内部含义是使 服务器控制的,就像 Cookie那样.
补充:
启发式缓存:主要是在请求中的expires字段,以服务器时间作为参考系, 其优先级比
Cache-Control:max-age
低, 两者同时出现在响应头时, Expires将被后者覆盖. 如果Expires,Cache-Control: max-age, 或 Cache-Control:s-maxage
都没有在响应头中出现, 并且也没有其它缓存的设置, 那么浏览器默认会采用一个启发式的算法, 通常会取响应头的Date_value - Last-Modified_value
值的10%作为缓存时间.
from memory cache、from disk cache、文件本身的大小(如:1.8k)之间的区别
1、from memory cache
:不会请求服务器,直接从内存中读取资源;当页面关闭之后,此资源就会被释放,再次重新打开相同的页面时不会出现from memory cache
的情况。
2、from disk cache
:不会请求服务器,直接从磁盘中读取资源;当页面关闭之后,此资源就不会被释放,再次重新打开相同的页面时会出现from disk cache
的情况。
3、1.8k,即资源本身的大小,当http状态码为200时是从浏览器获取资源的大小。当状态码为304时,是与服务端通信的报文的大小,并不是该资源本身的大小,是从本地读取资源的大小。
资源编译
1、index.html文件解析
index.html文件解析过程中在文章的开头已经阐述过了,这里就不再赘述了。
2、浏览器渲染过程
页面渲染的过程图
- HTML Parser: 是浏览器内核中的HTMlParser将HTML标签转化为DOM Tree
- CSS Parser:是浏览器内核中的CSSParser将CSS文件转化为样式规则
浏览器内核解析流程: 通过DOM tree和Style Rules进行Attachment(附加)在一起形成了Render Tree(渲染树),再通过Layout(布局引擎)再对Render Tree进行操作,最后通过Painting绘制之后回显到页面进行Display(显示)
在以上解析的过程中,针对生成DOM Tree的过程中,可能会存在许多的DOM操作,有许多的js文件需要执行。但是注意,JavaScript是高级程序语言,浏览器不能直接识别,需要通过JavaScript引擎进行转化为最终的机器指令来执行。(无论js是交给浏览器还是Node执行,其最终都需要通过CPU来进行执行,但是CPU只认识自己的指令集,所以需要借助JS引擎来转化)
浏览器内核: 浏览器内核主要由两部分组成,一个是浏览器的渲染引擎(render engine)和JS引擎。
渲染引擎:在生面图中的HTML Parser和 CSS Parser都是来自渲染引擎中。主要用于页面渲染。
JS引擎:用于对JavaScript代码的编译和解析,现在使用最多的应该就是v8引擎。
v8Engine解析流程
其中比较细节的几点:
-
scanner层到parser层中间会经过PreParser层,因为并不是所有的JavaScript代码在初始的时候都会进行执行,如果初始就进行解析,则会消耗大量时间,网页加载速率就会很慢,所以采用lazy parsing(延迟解析)。官网有句话Even though developers can delay such code with async and deferred scripts, that’s not always feasible.
-
在MachineCode中,有一步是进行Deoptimization(取消优化),即以下这段代码多次调用就会被标记成热点函数,在经过TurboFan转化成优化的机器码,这样做的目的是为了提高代码的执行性能。但是,如果因为js无法做类型校验,如果num1或者num2的类型变为了字符串,那么这个
+
的符号的意义就变了,之前优化好的机器码不能正常的处理运算,所以需要逆向的转为字节码。function sum(num1,num2){ return num1 + num2 }
-
AST(Abstract Syntax Tree)抽象语法树,其实这个概念不陌生。例如在vue的template编译创建VNode的时候,就会生成AST。在v8引擎中的,例如
const name = 'YDKD'
,经过AST之后,会形成下面的代码。
{
"type": "Program",
"start": 0,
"end": 18,
"body": [
{
"type": "VariableDeclaration",
"start": 0,
"end": 17,
"declarations": [
{
"type": "VariableDeclarator",
"start": 4,
"end": 17,
"id": {
"type": "Identifier",
"start": 4,
"end": 8,
"name": "name"
},
"init": {
"type": "Literal",
"start": 11,
"end": 17,
"value": "ydkd",
"raw": "'ydkd'"
}
}
],
"kind": "let"
}
],
"sourceType": "module"
}
附上本文相关参考地址: PreParser(预解析) Scanner(生成Tokens) Ignition(解释器) Ignition(编译器) 缓存相关