浏览器中输入 www.baidu.com 直到看到页面之间发生了什么?
今天就说说根据这道题梳理自己的前端知识!
解析 URL
解释 URL 是浏览器的活,所以首先要搞明白浏览器的工作方式。
浏览器是多进程的,有一个主控进程,进程可能包括主控进程,插件进程,GPU,tab 页(浏览器内核)等等,主要包括:
- Browser 进程:浏览器的主进程(负责协调、主控),只有一个
- 第三方插件进程:每种类型的插件对应一个进程,仅当使用该插件时才创建
- GPU 进程:最多一个,用于 3D 绘制
- 浏览器渲染进程(内核):默认每个 Tab 页面一个进程,互不影响,控制页面渲染,脚本执行,事件处理等(有时候会优化,如多个空白 tab 会合并成一个进程)
浏览器渲染进程是多线程的,它有主要几大类子线程
- GUI 线程
- JS 引擎线程
- 事件触发线程
- 定时器线程
- 网络请求线程
输入 URL 后,从浏览器会进行解析,URL 一般包括几大部分:
- protocol,协议头,譬如有 http,https 等
- host,主机域名或 IP 地址
- port,端口号
- path,目录路径
- query,即查询参数
- fragment,即 # 后的 hash 值,一般用来定位到某个位置
每次网络请求时都需要开辟单独的线程进行,览器会根据解析出得协议,开辟一个网络线程,前往请求资源.
http 缓存
网络请求之前会先都差缓存,HTTP 缓存有多种规则,根据是否需要重新向服务器发起请求来分类,将其分为强制缓存,对比缓存。
强缓存(200 from cache)时,浏览器如果判断本地缓存未过期,就直接使用,无需发起 http 请求
对比缓存(304)时,浏览器会向服务端发起 http 请求,然后服务端告诉浏览器文件未改变,让浏览器使用本地缓存,可以使用
Ctrl + F5强制刷新可以使得对比缓存无效
http1.0 中的缓存控制:
- Pragma:严格来说,它不属于专门的缓存控制头部,但是它设置 no-cache 时可以让本地强缓存失效(属于编译控制,来实现特定的指令,主要是因为兼容 http1.0,所以以前又被大量应用)
- Expires:服务端配置的,属于强缓存,用来控制在规定的时间之前,浏览器不会发出请求,而是直接使用本地缓存,注意,Expires 一般对应服务器端时间,如 Expires:Fri, 30 Oct 1998 14:19:41
- If-Modified-Since/Last-Modified:这两个是成对出现的,属于协商缓存的内容,其中浏览器的头部是 If-Modified-Since,而服务端的是 Last-Modified,它的作用是,在发起请求时,如果 If-Modified-Since 和 Last-Modified 匹配,那么代表服务器资源并未改变,因此服务端不会返回资源实体,而是只返回头部,通知浏览器可以使用本地缓存。Last-Modified,顾名思义,指的是文件最后的修改时间,而且只能精确到 1s 以内
http1.1 中的缓存控制:
Cache-Control:缓存控制头部,有 no-cache、max-age 等多种取值
- Max-Age:服务端配置的,用来控制强缓存,在规定的时间之内,浏览器无需发出请求,直接使用本地缓存,注意,Max-Age 是 Cache-Control 头部的值,不是独立的头部,譬如 Cache-Control: max-age=3600,而且它值得是绝对时间,由浏览器自己计算
If-None-Match/E-tag:这两个是成对出现的,属于协商缓存的内容,其中浏览器的头部是 If-None-Match,而服务端的是 E-tag,同样,发出请求后,如果 If-None-Match 和 E-tag 匹配,则代表内容未变,通知浏览器使用本地缓存,和 Last-Modified 不同,E-tag 更精确,它是类似于指纹一样的东西,基于 FileEtag INode Mtime Size 生成,也就是说,只要文件变,指纹就会变,而且没有 1s 精确度的限制。
强缓存判断依据
1 | (http1.1)Cache-Control => Cache-Control: max-age = 35600 |
cache-control 中的 max-age 保存一个相对时间。例如 Cache-Control: max-age = 35600,表示浏览器收到文件后,缓存在 35600s 内均有效。如果同时存在 cache-control 和 Expires ,浏览器总是优先使用 cache-control。
Max-Age 相比 Expires,Expires 使用的是服务器端的时间,但是有时候会有这样一种情况-客户端时间和服务端不同步。所以一般 http1.1 后不推荐使用 Expires。
对比缓存判断依据
1 | (http1.1)E-tag/If-None-Match |
last-modified 是第一次请求资源时,服务器返回的字段,表示最后一次更新的时间。下一次浏览器请求资源时就发送 if-modified-since 字段。服务器用本地 Last-modified 时间与 if-modified-since 时间比较,如果不一致则认为缓存已过期并返回新资源给浏览器;如果时间一致则发送 304 状态码,让浏览器继续使用缓存。
E-tag:资源的实体标识(哈希字符串),当资源内容更新时,E-tag 会改变。服务器会判断 E-tag 是否发生变化,如果变化则返回新资源,否则返回 304。
如果同时带有 E-tag 和 Last-Modified,服务端会优先检查 E-tag
在浏览器接收到服务器响应后,会检测响应头部(Header),如果有 nEtag 字段,那么浏览器就会将本次缓存写入硬盘中。
浏览器优先强缓存,如果设置了 no-cache 会走协商缓存
开启线程发出请求
- dns 查询
dns 是通常是完成域名到 ip 的映射,大致流程:
- 如果浏览器有缓存,直接使用浏览器缓存,否则使用本机缓存,再没有的话就是用 host
- 如果本地没有,就向 dns 域名服务器查询(当然,中间可能还会经过路由,也有缓存等),查询到对应的 IP
dns 解析是很耗时的,因此如果解析域名过多,会让首屏加载变得过慢,可以考虑 dns-prefetch 优化
1 | a 标签的 DNS 预解析 默认是开启的 但是当 使用 https 的时候是默认关闭的需要手动开启 a 标签的预解析 |
- tcp/ip 请求
首先会建立 tcp 连接
- 建立链接(三次握手)
1 | 客户端发个请求“开门呐,我要进来”给服务器 |
建立连接也会断开连接只是不是这时候断开,先提前说下
- 断开连接(四次挥手)
1 | 客户端发个“时间不早了,我要走了”给服务器,等服务器起身送他 |
由于 tcp/ip 对链接有并发的控制,所以有甚多针对请求的优化,比如合并请求的雪碧图。
关于这里有涉及到五层网络协议,就是从客户端发出 http 请求到服务器接收,中间会经过一系列的流程。
1 | 应用层(dns,http) DNS 解析成IP并发送 http 请求 |
其实也有一个完整的 OSI 七层框架,与之相比,多了会话层、表示层。
浏览器向服务器发送 HTTP 请求。
- http 报文结构
报文一般包括了:通用头部,请求/响应头部,请求/响应体
- 通用头部
1 | Request Url: 请求的web服务器地址 |
code 状态码
1 | 1xx——指示信息,表示请求已接收,继续处理 |
请求/响应头部
常用的请求头部(部分):
1 | Accept: 接收类型,表示浏览器支持的MIME类型(对标服务端返回的 Content-Type ) |
常用的响应头部(部分):
1 | Access-Control-Allow-Headers: 服务器端允许的请求 Headers |
一般来说,请求头部和响应头部是匹配分析的。
譬如,请求头部的 Accept 要和响应头部的 Content-Type 匹配,否则会报错
譬如,跨域请求时,请求头部的 Origin 要匹配响应头部的 Access-Control-Allow-Origin,否则会报跨域错误
譬如,在使用缓存时,请求头部的 If-Modified-Since、If-None-Match 分别和响应头部的 Last-Modified、E-tag 对应
服务器接收到请求
- 负载均衡
对于大型的项目,由于并发访问量很大,所以往往一台服务器是吃不消的,所以一般会有若干台服务器组成一个集群,然后配合反向代理实现负载均衡。用户发起的请求都指向调度服务器(反向代理服务器,譬如安装了 nginx 控制负载均衡),然后调度服务器根据实际的调度算法,分配不同的请求给对应集群中的服务器执行,然后调度器等待实际服务器的 HTTP 响应,并将它反馈给用户
后台的处理
cookie
cookie 是浏览器的一种本地存储方式,一般用来帮助客户端和服务端通信的,常用来进行身份校验,结合服务端的 session 使用.
由于在同域名的资源请求时,浏览器会默认带上本地的 cookie,针对这种情况,在某些场景下是需要优化,例如:将静态资源分组,分别放到不同的子域名下
- 而子域名请求时,是不会带上父级域名的 cookie 的,所以就避免了浪费
说到了多域名拆分,这里再提一个问题,那就是:
在移动端,如果请求的域名数过多,会降低请求速度(因为域名整套解析流程是很耗费时间的,而且移动端一般带宽都比不上 pc)此时就需要用到一种优化方案:dns-prefetch(让浏览器空闲时提前解析 dns 域名,不过也请合理使用,勿滥用)
说到 cookie 就要把 localStorage,sessionStorage 区分下:
1 | cookie 数据始终在同源的 http 请求中携带(即使不需要),即 cookie 在浏览器和服务器间来回传递。而 sessionStorage 和 localStorage 不会自动把数据发给服务器,仅在本地保存。 |
- gzip 压缩
gzip 是 GNU zip 的缩写,它是一个 GNU 自由软件的文件压缩程序。一般服务器都会开启 gzip 这样可以减少带宽消耗。但是 HTTP 压缩需要成本。Web 服务器获得需要的内容,然后压缩它,最后将它发送到客户端。如果内容不能被进一步压缩,你只是在浪费 CPU 做无意义的任务,采用 HTTP 压缩已经被过压缩的东西并不能使它更小。事实上,添加标头,压缩字典,并校验响应体实际上使它变得更大。
- 长连接与短连接
首先看 tcp/ip 层面的定义:
- 长连接:一个 tcp/ip 连接上可以连续发送多个数据包,在 tcp 连接保持期间,如果没有数据包发送,需要双方发检测包以维持此连接,一般需要自己做在线维持(类似于心跳包)
- 短连接:通信双方有数据交互时,就建立一个 tcp 连接,数据发送完成后,则断开此 tcp 连接
然后在 http 层面:
- http1.0 中,默认使用的是短连接,也就是说,浏览器每进行一次 http 操作,就建立一次连接,任务结束就中断连接,譬如每一个静态资源请求时都是一个单独的连接
- http1.1 起,默认使用长连接,使用长连接会有这一行 Connection: keep-alive,在长连接的情况下,当一个网页打开完成后,客户端和服务端之间用于传输 http 的 tcp 连接不会关闭,如果客户端再次访问这个服务器的页面,会继续使用这一条已经建立的连接
注意: keep-alive 不会永远保持,它有一个持续时间,一般在服务器中配置(如 apache),另外长连接需要客户端和服务器都支持时才有效
- https
简单来看,https 与 http 的区别就是: 在请求前,会建立 ssl 链接,确保接下来的通信都是加密的,无法被轻易截取分析。如果要将网站升级成 https,需要后端支持(后端需要申请证书等),然后 https 的开销也比 http 要大(因为需要额外建立安全链接以及加密等),所以一般来说 http2.0 配合 https 的体验更佳(因为 http2.0 更快了)SSL/TLS 的握手流程握手流程:
1 | 1. 浏览器建立 SSL 连接,像服务端 发送一个随机数和加密方法 |
- http 2.0
http2.0 它相当于是 http 的下一代规范,与 http1.1 的显著不同点:
- http1.1 中,每请求一个资源,都是需要开启一个 tcp/ip 连接的,所以对应的结果是,每一个资源对应一个 tcp/ip 请求,由于 tcp/ip 本身有并发数限制,所以当资源一多,速度就显著慢下来
- ttp2.0 中,一个 tcp/ip 请求可以请求多个资源,也就是说,只要一次 tcp/ip 请求,就可以请求若干个资源,分割成更小的帧请求,速度明显提升。
http2.0 的一些特性:
- 多路复用(即一个 tcp/ip 连接可以请求多个资源)
- 首部压缩(http 头部压缩,减少体积)
- 二进制分帧(在应用层跟传送层之间增加了一个二进制分帧层,改进传输性能,实现低延迟和高吞吐量)
- 服务器端推送(服务端可以对客户端的一个请求发出多个响应,可以主动通知客户端)
- 请求优先级(如果流被赋予了优先级,它就会基于这个优先级来处理,由服务器决定需要多少资源来处理该请求。)
注意:HTTP2.0 的多路复用和 HTTP1.1 中的长连接复用的区别就是:
HTTP/1.1 Pipeling 解决方式为,若干个请求排队串行化单线程处理,后面的请求等待前面请求的返回才能获得执行机会,一旦有某请求超时等,后续请求只能被阻塞,毫无办法,也就是人们常说的线头阻塞。
HTTP/2 多个请求可同时在一个连接上并行执行。某个请求任务耗时严重,不会影响到其它连接的正常执行;
浏览器接收响应
构建渲染树
浏览器进行解析渲染呈现给用户。整个过程涉及两个方面:解析和渲染。在渲染页面之前,需要构建 DOM 树和 CSSOM 树。
- HTML 解析,构建 DOM
解析 HTML 到构建出 DOM 当然过程可以简述如下:
Bytes → characters → tokens → nodes → DOM
- 生成 CSS 规则
CSS 规则树的生成也是类似。简述为:
Bytes → characters → tokens → nodes → CSSOM
- 渲染
布局是由 CPU 处理的,而绘制则是由 GPU 完成的,GPU 会对我们的渲染层作缓存,如果我们把那些一直发生大量重排重绘的元素提取出来,单独触发一个渲染层,那样这个元素不就不会“连累”其他元素一块重绘。
Video 元素、WebGL、Canvas、CSS3 3D、CSS 滤镜、z-index 大于某个相邻节点的元素都会触发新的 Layer,如果图层中某个元素需要重绘,那么整个图层都需要重绘。比如一个图层包含很多节点,其中有个 gif 图,gif 图的每一帧,都会重回整个图层的其他节点,然后生成最终的图层位图。所以这需要通过特殊的方式来强制 gif 图属于自己一个图层,就是给某个元素加上下面的样式:
1 | transform: translateZ(0); |
也会触发渲染层,把容易触发重排重绘的元素单独触发渲染层,让它与那些“静态”元素隔离,让 GPU 分担更多的渲染工作,我们通常把这样的措施成为硬件加速,或者是 GPU 加速。
1 | Style:该区域为样式计算阶段,浏览器会根据选择器(就是CSS选择器,如.td)计算出哪些节点应用哪些CSS规则,然后计算出每个节点的最终样式并应用到节点上。 |
如果动态修改了 DOM 或 CSS,就会重新布局(Layout)或渲染(Repaint)这里 Layou t 和 Repaint 的概念是有区别的:
Layout,也称为 Reflow,即回流。一般意味着元素的内容、结构、位置或尺寸发生了变化,需要重新计算样式和渲染树
Repaint,即重绘。意味着元素发生的改变只是影响了元素的一些外观之类的时候(例如,背景色,边框颜色,文字颜色等),此时只需要应用新样式绘制这个元素就可以了
什么会引起回流?
1 | 1.页面渲染初始化 |
回流一定伴随着重绘,重绘却可以单独出现
所以一般会有一些优化方案,如:
- 减少逐项更改样式,最好一次性更改 style,或者将样式定义为 class 并一次性更新
- 避免循环操作 dom,创建一个 documentFragment 或 div,在它上面应用所有 DOM 操作,最后再把它添加到 window.document
- 避免多次读取 offset 等属性。无法避免则将它们缓存到变量
- 将复杂的元素绝对定位或固定定位,使得它脱离文档流,否则回流代价会很高
最后如果想要知道每个 CSS 属性将会对哪个阶段产生怎样的影响,请去 CSS Triggers,该网站详细地说明了每个 CSS 属性会影响到哪个阶段。
- 关闭 TCP 连接或继续保持连接
通过四次挥手关闭连接