在写这篇前端散记之前有写过另外一篇散记,可点击 前端散记 访问。所谓散记,东西都比较零散,更谈不上什么深入,但是至少可以让读者知道一些概念理论,如果深入可以自行去查询相关知识。
keep-alive vs http2
有很多文章都说 http2 相比 http1.1 增加了连接复用。这句话其实是不准确的。
在 HTTP 1.1 中 所有的连接默认都是持续连接,除非特殊声明不支持。 而在 http1.0 中,官方没有支持 keep-alive, 通常会手动在请求头中添加 Connection: Keep-Alive。
keep-alive 就是 TCP 连接复用的开端。改善的效果就是不再重新建立TCP连接,省去 三次握手 的时间。如下图:
优势有:
- 较少的CPU和内存的使用(由于同时打开的连接的减少了);
- 允许请求和应答的HTTP管线化;
- 降低拥塞控制 (TCP连接减少了);
- 减少了后续请求的延迟(无需再进行握手);
- 报告错误无需关闭TCP连接。
http pipelining
有些文章中会有一个误区,就是TCP连接必须等一个请求响应完成后,才能复用。这是不对的,但其实可以注意上面优势里提到到 http pipelining,如下图:
HTTP1.1 中,一个TCP连接里是可以同时发送(实际有先后,但可以在响应前)多个请求的。但它是有序的,遵循先进先出,服务端只能按顺序响应请求(如果前面的请求没有响应完成或需要很长时间,后面的请求就会被阻塞),所以可能发生 队头阻塞(HOL blocking),造成延迟。
连续的 GET 和 HEAD 请求总可以管线化的。一个连续的幂等请求,如 GET,HEAD,PUT,DELETE,是否可以被管线化取决于一连串请求是否依赖于其他的。
所以keep-alive 的劣势也很明显:
- Keep-Alive可能会极大的影响服务器性能,因为它在文件被请求之后还保持了不必要的连接很长时间;
- 可能发生队头阻塞(HOL blocking),造成延迟。
HTTP2
HTTP2 主要解决的问题也是 TCP连接复用。但它比 keep-alive 更彻底,类似于通信工程里的时分复用,多个请求可以同时发送(不分先后),同时响应,解决了 队头阻塞(HOL blocking)的问题,极大提高效率。
keep-alive 的 HTTP pipelining 相当于单线程的,而 HTTP2 相当于并发。
HTTP2 的优点:
- 对HTTP头字段进行数据压缩(即HPACK算法);
- HTTP/2 服务端推送(Server Push);
- 请求管线化;
- 修复HTTP/1.0版本以来未修复的队头阻塞问题;
- 对数据传输采用多路复用,让多个请求合并在同一 TCP连接内。
后三个优点其实都是多路复用带来的优点。
HTTP3相关
HTTP/3 新特性
1. HTTP/3简介
虽然 HTTP/2 解决了很多之前旧版本的问题,但是它还是存在一个巨大的问题,主要是底层支撑的 TCP 协议造成的。
上文提到 HTTP/2 使用了多路复用,一般来说同一域名下只需要使用一个 TCP 连接。但当这个连接中出现了丢包的情况,那就会导致 HTTP/2 的表现情况反倒不如 HTTP/1 了。
因为在出现丢包的情况下,整个 TCP 都要开始等待重传,也就导致了后面的所有数据都被阻塞了。但是对于 HTTP/1.1 来说,可以开启多个 TCP 连接,出现这种情况反到只会影响其中一个连接,剩余的 TCP 连接还可以正常传输数据。
那么可能就会有人考虑到去修改 TCP 协议,其实这已经是一件不可能完成的任务了。因为 TCP 存在的时间实在太长,已经充斥在各种设备中,并且这个协议是由操作系统实现的,更新起来不大现实。
基于这个原因,Google 就更起炉灶搞了一个基于 UDP 协议的 QUIC 协议,并且使用在了 HTTP/3 上,HTTP/3 之前名为 HTTP-over-QUIC,从这个名字中我们也可以发现,HTTP/3 最大的改造就是使用了 QUIC。
QUIC 虽然基于 UDP,但是在原本的基础上新增了很多功能,接下来我们重点介绍几个QUIC新功能。
2. QUIC新功能
- 0-RTT
通过使用类似 TCP 快速打开的技术,缓存当前会话的上下文,在下次恢复会话的时候,只需要将之前的缓存传递给服务端验证通过就可以进行传输了。0RTT 建连可以说是 QUIC 相比 HTTP2 最大的性能优势。那什么是 0RTT 建连呢?
这里面有两层含义:
- 传输层 0RTT 就能建立连接。
- 加密层 0RTT 就能建立加密连接。
上图左边是 HTTPS 的一次完全握手的建连过程,需要 3 个 RTT。就算是会话复用也需要至少 2 个 RTT。
而 QUIC 呢?由于建立在 UDP 的基础上,同时又实现了 0RTT 的安全握手,所以在大部分情况下,只需要 0 个 RTT 就能实现数据发送,在实现前向加密的基础上,并且 0RTT 的成功率相比 TLS 的会话记录单要高很多。
- 多路复用
虽然 HTTP/2 支持了多路复用,但是 TCP 协议终究是没有这个功能的。QUIC 原生就实现了这个功能,并且传输的单个数据流可以保证有序交付且不会影响其他的数据流,这样的技术就解决了之前 TCP 存在的问题。
同HTTP2.0一样,同一条 QUIC连接上可以创建多个stream,来发送多个HTTP请求,但是,QUIC是基于UDP的,一个连接上的多个stream之间没有依赖。比如下图中stream2丢了一个UDP包,不会影响后面跟着 Stream3 和 Stream4,不存在 TCP 队头阻塞。虽然stream2的那个包需要重新传,但是stream3、stream4的包无需等待,就可以发给用户。
另外QUIC 在移动端的表现也会比 TCP 好。因为 TCP 是基于 IP 和端口去识别连接的,这种方式在多变的移动端网络环境下是很脆弱的。但是 QUIC 是通过 ID 的方式去识别一个连接,不管你网络环境如何变化,只要 ID 不变,就能迅速重连上。
- 加密认证的报文
TCP 协议头部没有经过任何加密和认证,所以在传输过程中很容易被中间网络设备篡改,注入和窃听。比如修改序列号、滑动窗口。这些行为有可能是出于性能优化,也有可能是主动攻击。
但是 QUIC 的 packet 可以说是武装到了牙齿。除了个别报文比如 PUBLIC_RESET 和 CHLO,所有报文头部都是经过认证的,报文 Body 都是经过加密的。
这样只要对 QUIC 报文任何修改,接收端都能够及时发现,有效地降低了安全风险。
如上图所示,红色部分是 Stream Frame 的报文头部,有认证。绿色部分是报文内容,全部经过加密。
- 向前纠错机制
QUIC协议有一个非常独特的特性,称为向前纠错 (Forward Error Correction,FEC),每个数据包除了它本身的内容之外,还包括了部分其他数据包的数据,因此少量的丢包可以通过其他包的冗余数据直接组装而无需重传。向前纠错牺牲了每个数据包可以发送数据的上限,但是减少了因为丢包导致的数据重传,因为数据重传将会消耗更多的时间(包括确认数据包丢失、请求重传、等待新数据包等步骤的时间消耗)。
假如说这次我要发送三个包,那么协议会算出这三个包的异或值并单独发出一个校验包,也就是总共发出了四个包。当出现其中的非校验包丢包的情况时,可以通过另外三个包计算出丢失的数据包的内容。当然这种技术只能使用在丢失一个包的情况下,如果出现丢失多个包就不能使用纠错机制了,只能使用重传的方式了。
总结
HTTP/1.x 有连接无法复用、队头阻塞、协议开销大和安全因素等多个缺陷;
HTTP/2 通过多路复用、二进制流、Header 压缩等等技术,极大地提高了性能,但是还是存在着问题的;
QUIC 基于 UDP 实现,是 HTTP/3 中的底层支撑协议,该协议基于 UDP,又取了 TCP 中的精华,实现了即快又可靠的协议。
小程序优化
以本人所项接触的项目(IM相关)实战为例
- 去除后端返回来的冗余字段;
- 状态管理(以为项目使用的是
mpVue
小程序开发框架) ; - 图片资源优化(采用 CDN);
- 图片使用 CDN 之后发现网络请求多了还是给人感觉体验不好,所以又采取了部分较小的图片直接打包进了项目 JS,由于担心小程序项目大小过大,所以才去分包;
- 群组人员 ID 缓存,并通过 ID 数组是否变化来加载人员信息。
Webpack 优化
- externals + cdn;
- cache-loader;
- happy-pack、thread-loader;
- webpack-bundle-analyzer
- Tree Shaking
- CommonChunkPlugin (Webpack 4.0移除,使用 SplitChunkPlugin 替代);
- DllPlugin 和 DllReferencePlugin:DLLPlugin 就是将包含大量复用模块且不会频繁更新的库进行编译,只需要编译一次,编译完成后存在指定的文件(这里可以称为动态链接库)中。在之后的构建过程中不会再对这些模块进行编译,而是直接使用 DllReferencePlugin 来引用动态链接库的代码。
浏览器渲染页面的过程
从耗时的角度,浏览器请求、加载、渲染一个页面,时间花在下面五件事情上:
- DNS 查询
- TCP 连接
- HTTP 请求即响应
- 服务器响应
- 客户端渲染
浏览器对内容的渲染,这一部分(渲染树构建、布局及绘制),又可以分为下面五个步骤:
- 处理 HTML 标记并构建 DOM 树。
- 处理 CSS 标记并构建 CSSOM 树。
- 将 DOM 与 CSSOM 合并成一个渲染树。
- 根据渲染树来布局,以计算每个节点的几何信息。
- 将各个节点绘制到屏幕上。
async/await 与 Generator 区别
ES7 提出的async 函数,终于让 JavaScript 对于异步操作有了终极解决方案。No more callback hell。
async 函数是 Generator 函数的语法糖。使用 关键字 async 来表示,在函数内部使用 await 来表示异步。
想较于 Generator,Async 函数的改进在于下面四点:
- 内置执行器。Generator 函数的执行必须依靠执行器,而 Aysnc 函数自带执行器,调用方式跟普通函数的调用一样;
- 更好的语义。async 和 await 相较于 * 和 yield 更加语义化;
- 更广的适用性。co 模块约定,yield 命令后面只能是 Thunk 函数或 Promise对象。而 async 函数的 await 命令后面则可以是 Promise 或者 原始类型的值(Number,string,boolean,但这时等同于同步操作);
- 返回值是 Promise。async 函数返回值是 Promise 对象,比 Generator 函数返回的 Iterator 对象方便,可以直接使用 then() 方法进行调用。
JSONP 封装
以下是之前在做跨域项目所封装的 jsonp 库
1 | function jsonp(options) { |
平民版深度拷贝
比乞丐版
JSON.parse(JSON.stringify(obj))
好点的深度拷贝
1 | /** |
instanceof 实现
1 | function new_instance_of(leftVaule, rightVaule) { |
new 封装
1 | function myNew(Con, ...args) { |
手写 Promise (乞丐版)
1 | function MyPromise (executor) { |
flex
- flex-grow属性
flex-grow属性定义项目的放大比例,默认为0,即如果存在剩余空间,也不放大。
1 | .item { |
如果所有项目的flex-grow属性都为1,则它们将等分剩余空间(如果有的话)。如果一个项目的flex-grow属性为2,其他项目都为1,则前者占据的剩余空间将比其他项多一倍。
- flex-shrink属性
flex-shrink属性定义了项目的缩小比例,默认为1,即如果空间不足,该项目将缩小。
1 | .item { |
如果所有项目的flex-shrink属性都为1,当空间不足时,都将等比例缩小。如果一个项目的flex-shrink属性为0,其他项目都为1,则空间不足时,前者不缩小。
负值对该属性无效。
- flex-basis属性
flex-basis属性定义了在分配多余空间之前,项目占据的主轴空间(main size)。浏览器根据这个属性,计算主轴是否有多余空间。它的默认值为auto,即项目的本来大小。
1 | .item { |
它可以设为跟width或height属性一样的值(比如350px),则项目将占据固定空间。