读《Web性能优化与HTTP/2》有感笔记

一、前言

前段时间本着尝鲜与想释放看书的欲望的初衷入手了Kindle PaperWhite 3,买来后便把之前一直想看但迫于书籍的沉重与携带的不便而没看的书籍塞了进去,其中有一本叫做《Web性能优化与HTTP/2》,这是从看云上找到的一本书籍,被题目所吸引,但是放入后才发现这本书中并没有多少字,但是牵扯出的东西却太多了,所以打算写这么一篇,记录一下自己的感受与学习。

下面以书中所提及的知识点为主线,记录我对于各个知识点的学习与感受

二、Http 304

304 Not Modified是一个在网页浏览过程中不会直接发现的一个提示,在正常浏览网页的时候用户不可见,只有当我们打开Console控制台的时候才会发现,请求列表中存在304响应状态码。

如果客户端发送了一个带条件的GET请求且该请求已被允许,而文档的内容(自上次访问以来或者根据请求的条件)并没有改变,则服务器应当返回这个状态码。304响应禁止包含消息体,因此始终以消息头后的第一个空行结尾。

2.1、Http 304的响应状态的资源更新机制:

  • 可能请求一:当客户端缓存了目标资源但不确定该缓存资源是否是最新版本的时候, 就会发送一个条件请求,这样就可以辨别出一个请求是否是条件请求,在进行条件请求时,304请求的响应头信息里面有两个比较重要的请求头字段:If-Modified-Since【其值为服务器上次返回的Last-Modified响应头中的Date日期值】和 If-None-Match【其值为服务器上次返回的ETag响应头的值】,这两个字段表示发送的是一个条件请求。
  • 结果一:服务器会读取到这两个请求头中的值,判断出客户端缓存的资源是否是最新的,如果是的话,服务器就会返回HTTP/304 Not Modified响应头, 但没有响应体。客户端收到304响应后,就会从本地缓存中读取对应的资源.
  • 结果二:服务器认为客户端缓存的资源已经过期了,那么服务器就会返回HTTP/200 OK响应,响应体就是该资源当前最新的内容。客户端收到200响应后,就会用新的响应体覆盖掉旧的缓存资源。
  • 可能请求二:如果客户端第一次请求该资源或者请求该资源的响应头不存在了Last-Modified和ETag请求头字段,则必须无条件(unconditionally)请求该资源,服务器也就必须返回完整的资源数据。

2.2、使用条件请求机制的原因:

  • 因为可以省去传输整个响应体的时间,所以条件请求可以加速网页的打开时间,但仍然会有网络延迟,因为浏览器还是得为每个资源生成一条条件请求,并且等到服务器返回HTTP/304响应,才能读取缓存来显示网页。

2.3、其他可用策略:

  • 如果服务器在响应上指定Cache-Control或Expires指令,这样客户端就能知道该资源的可用时间为多长,也就能跳过条件请求的步骤,直接使用缓存中的资源了。

三、gzip压缩Http body

gzip 是 GNUzip 的缩写,最早用于 UNIX 系统的文件压缩。HTTP 协议上的 gzip 编码是一种用来改进 web 应用程序性能的技术,web 服务器和客户端(浏览器)必须共同支持 gzip。目前主流的浏览器,Chrome,firefox,IE 等都支持该协议。常见的服务器如 Apache,Nginx,IIS 同样支持 gzip。gzip压缩比率在3到10倍左右,可以大大节省服务器的网络带宽。而在实际应用中,并不是对所有文件进行压缩,通常只是压缩静态文件。

3.1、Web服务器处理HTTP压缩的过程图解:

Web服务器处理HTTP压缩的过程

四、HSTS策略

HTTP严格传输安全(英语:HTTP Strict Transport Security,缩写:HSTS)是一套由互联网工程任务组发布的互联网安全策略机制。网站可以选择使用HSTS策略,来让浏览器强制使用HTTPS与网站进行通信,以减少会话劫持风险。

4.1、HSTS策略的作用以使用说明

HSTS的作用是强制客户端(如浏览器)使用HTTPS与服务器创建连接。服务器开启HSTS的方法是,当客户端通过HTTPS发出请求时,在服务器返回的超文本传输协议响应头中包含Strict-Transport-Security字段。非加密传输时设置的HSTS字段无效。

比如,https://www.bugwz.com 的响应头含有Strict-Transport-Security: max-age=31536000; includeSubDomains。这意味着两点:

  • 在接下来的一年(即31536000秒)中,浏览器只要向example.com或其子域名发送HTTP请求时,必须采用HTTPS来发起连接。比如,用户点击超链接或在地址栏输入 https://www.bugwz.com ,浏览器应当自动将 http 转写成 https,然后直接向 https://www.bugwz.com 发送请求。
  • 在接下来的一年中,如果 https://www.bugwz.com 服务器发送的TLS证书无效,用户不能忽略浏览器警告继续访问网站。

4.2、HSTS策略的一些问题

HSTS策略在它看到STS头部声明的max-age的期间内保护了客户端从Http到https跳转的过程中的可能的被拦截。然而,HSTS并不是http回话劫持的完美解决方案。用户在访问HSTS保护的网站时,在以下情况下仍然容易受到攻击:

  • 以前从未访问过该网站
  • 最近重新安装了其操作系统
  • 最近重新安装了其浏览器
  • 切换到新的浏览器
  • 切换到一个新的设备如移动电话
  • 删除浏览器的缓存
  • 最近没访问过该站并且max-age过期了

为了解决这个问题,Google坚持维护了一个"HSTS preload list"的站点域名和子域名,并通过https://hstspreload.appspot.com/【需要额外的手段才可以顺畅访问】提交其域名。该域名列表被分发和硬编码到主流的web浏览器。客户端访问此列表中的域名将主动的使用HTTPS,并拒绝使用HTTP访问该站点。
一旦设置了STS头部或者提交了你的域名到HSTS预加载列表,这是不可能将其删除的。这是一个单向不可逆的决定了你的域名必须通过Https进行访问的方法。

五、资源预加载

当我们访问一个页面的时候,该页面可能有一些资源存在很大的几率被用户点击查看,那么我们就可能需要对这些资源进行预加载,例如《Web性能优化与HTTP/2》这本书中所说的DNS预解析,这就可以减少一些DNS解析时间,提升用户访问的体验。资源预加载这种做法曾经被称为prebrowsing,但这并不是一项单一的技术,可以细分为几个不同的技术:DNS-prefetchsubresource 和标准的 prefetchpreconnectprerender

5.1、DNS 预解析 DNS-Prefetch

当你浏览一个网页的时候,浏览器会在加载网页时对网页中包含的域名进行解析缓存,这样在你单击当前已经加载完成的网页中的链接时就无需再进行DNS 回源解析,减少用户的等待时间,提高用户体验。

操作方法跟简单,只需要在文档顶部的 标签中加入以下代码(例如:其中的host可以为bugwz.com):

<link rel="dns-prefetch" href="//host/" />

这似乎是一个非常微小的性能优化,显得也并非那么重要,但事实并非如此 – Chrome 一直都做了类似的优化。实际上,单纯执行 DNS-Prefetch 只能够微小的提升浏览性能,因为大部分现代浏览器也都内置了预解析的功能,甚至在你在地址栏输入域名时就完成了预解析。通过阅读Chormium 的文档,得到以下信息:

  • 不用对超链接做手动 dns prefetching,因为 chrome 会自动做 dns prefetching
  • chrome 会自动把当前页面的所有带 href 的 link 的 dns 都 prefetch 一遍
  • 对于一些需要跳转的域名做好预解析,最多可以减少 300~500ms 的加载时间

兼容性展示:
DNS-Prefetch

5.2、预连接 Preconnect

与 DNS 预解析类似,preconnect 不仅完成 DNS 预解析,同时还将进行 TCP 握手和建立传输层协议。预先建立 socket 连接,从而消除昂贵的 DNS 查找、TCP 握手和 TLS 往返开销。使用方法是在文档顶部的 标签中加入以下代码:

<link rel="preconnect" href="https://bugwz.com" />

兼容性展示:
预连接 Preconnect

5.3、预获取 Prefetching

如果我们确定某个资源将来一定会被使用到,我们可以让浏览器预先请求该资源并放入浏览器缓存中。例如,一个图片和脚本或任何可以被浏览器缓存的资源,使用方法是在文档顶部的 标签中加入以下代码:

<link rel="prefetch" href="image.png" />

Prefetching 有两种用法。其中 prefetch 为将来的页面提供了一种低优先级的资源预加载方式,而 subresource 为当前页面提供了一种高优先级的资源预加载。所以,如果资源是当前页面必须的,或者资源需要尽快可用,那么最好使用 subresource。用法如下:

<link rel="subresource" href="styles.css" />

注意:与 DNS 预解析不同,预获取真正请求并下载了资源,并储存在缓存中。但预获取还依赖于一些条件,某些预获取可能会被浏览器忽略,例如从一个非常缓慢的网络中获取一个庞大的字体文件。并且,Firefox 只会在浏览器闲置时进行资源预获取。目前,字体文件必须等到 DOM 和 CSS 构建完成之后才开始下载,使用预获取就可以轻松绕过该瓶颈。

兼容性展示:
预连接 Preconnect
预获取 subresource

5.4、预渲染 Prerender

这是一个核武器,因为 prerender 可以预先加载文档的所有资源,代码如下:

<link rel="prerender" href="https://bugwz.com/" />

这类似于在一个隐藏的 tab 页中打开了某个链接 – 将下载所有资源、创建 DOM 结构、完成页面布局、应用 CSS 样式和执行 JavaScript 脚本等。当用户真正访问该链接时,隐藏的页面就切换为可见,使页面看起来就是瞬间加载完成一样。Google 搜索在其即时搜索页面中已经应用该技术多年了,微软也宣称在 IE11 中支持该特性。

需要注意的问题:

  • 不要滥用该特性,当你知道用户一定会点击某个链接时才可以进行预渲染,因为预加载的开销(抢占 CPU 资源,消耗电池,浪费带宽等)是高昂的,所以必须谨慎行事
  • 使用 Page Visibility API 可以防止页面真正可见前被执行

兼容性展示:
预渲染 Prerender

5.5、Preload

preload 是一个新规范,与 prefetch 不同(可能被忽略)的是,浏览器一定会预加载该资源,使用代码如下:

<link rel="preload" href="image.png" />

兼容性展示:
Preload

六、手动管理缓存localStorage

localStorage是HTML5中的特性,来实现手动控制缓存。大概的思路是,在定义模块时,同时将模块的代码和版本号分别储存到localStorage,在下一次打算请求模块之前,我们先判断模块的最新版本是不是在localStorage中,将不存在的模块组合在一起,请求动态合并的资源。

  • Cookie:Cookie的大小限制为4KB左右,是网景公司的前雇员 Lou Montulli 在1993年3月的发明。它的主要用途有保存登录信息,比如你登录某个网站市场可以看到“记住密码”,这通常就是通过在 Cookie 中存入一段辨别用户身份的数据来实现的。
  • LocalStorage:LocalStorage 是 HTML5 标准中新加入的技术,它并不是什么划时代的新东西,早在 IE 6 时代,就有一个叫 userData 的东西用于本地存储,而当时考虑到浏览器兼容性,更通用的方案是使用 Flash。而如今,localStorage 被大多数浏览器所支持。创建的代码实例如下:
localStorage.lastname="Smith";
document.write(localStorage.lastname);
  • sessionStorage:sessionStorage 与 localStorage 的接口类似,但保存数据的生命周期与 localStorage 不同。做过后端开发的同学应该知道 Session 这个词的意思,直译过来是“会话”。而 sessionStorage 是一个前端的概念,它只是可以将一部分数据在当前会话中保存下来,刷新页面数据依旧存在。但当页面关闭后,sessionStorage 中的数据就会被清空。创建的代码实例如下:
sessionStorage.lastname="Smith";
document.write(sessionStorage.lastname);
  • 三者对比详情如下所示:
    sessionStorage:sessionStorage 与 localStorage对比

6.2、需要注意的一些地方

  • 严禁将一些敏感数据放置在Cookie、localStorage 和 sessionStorage 中,因为只要打开Console控制台我就可以查看并修改这些存储在本地的值。
  • 假如同域下的其他页面被XSS攻击,攻击者就可以篡改localStorage的内容,可能导致原来的页面代码被植入恶意程序。
  • 在执行每个网页模块之前,需要计算一下代码摘要,对比下服务器给的该模块的摘要,再决定是否使用,也可以使用SRI策略(关于SRI策略的详解信息,可移步这里),由浏览器帮你做校验。

七、HTTP持久连接 keep alive和persistent

HTTP持久连接可以避免每次都经历缓慢的连接建立阶段,减少三次握手的RTT延迟,以及每次都执行关闭操作,节省耗时和带宽;避免TCP连接慢启动特性的拥塞适应阶段,从而利用重用TCP连接这一措施加速数据传输。一个客户端对任何服务器或代理最多只能维护两条持久连接,以防服务器过载。HTTP持久连接的两种类型为:

  • HTTP/1.0+ "keep-alive"连接
  • HTTP/1.1 "persistent"连接

7.1、HTTP/1.0+ keep-alive连接

HTTP/1.0+中支持的是keep-alive连接,keep-alive握手过程如下所示:

  • HTTP/1.0+支持keep-alive连接,但默认并未激活。客户端通过发送一个包含Connection: Keep-Alive首部的请求来请求服务器激活keep-alive连接,即将这条连接保持在打开状态。

  • 如果服务器愿意为下一条请求重用此连接,就会在响应中包含相同的首部。若没有,服务器就会在发回响应报文后关闭连接。客户端就是通过检测响应中是否包含Connection: Keep-Alive响应首部来判断服务器是否会在发送响应后关闭连接

  • 假如服务器同意使用keep-alive连接,那么接下来客户端必须在所有希望保持持久连接的请求中包含Connection: Keep-Alive首部。如果没有发送该首部,服务器会在那条请求后关闭连接。

  • 注意,Connection: Keep-Alive首部只是请求将连接保持在活跃状态。即使服务器和客户端都同意建立持久连接了,它们仍可以在任意时刻关闭空闲的keep-alive连接,且可随意限制keep-alive连接所处理事务的数量。我们可以通过Keep-Alive选项调节它们的行为,具体请看下一部分。

Keep-Alive选项解释说明:

Connection: Keep-Alive
Keep-Alive: max=5, timeout=120
  • 参数timeout:在Keep-Alive响应首部中发送,告诉客户端服务器估计会在打开状态保持到连接空闲多长时间后关闭连接。
  • 参数max:在Keep-Alive响应首部中发送,告诉客户端服务器还会为另外几个http事务将连接保持在打开状态。
  • 注意,这两个参数值仅仅是估计,并非承诺。

7.2、HTTP/1.1的persistent连接

  • HTTP/1.1逐渐停止了对keep-alive连接的支持,用persistent连接替代了它,与keep-alive连接不同,HTTP/1.1中persistent连接默认就是激活的,除非特别指明,否则HTTP/1.1认为所有连接都是持久的。

  • HTTP/1.1的客户端假定在收到的响应后,除非报文包含了Connection: Close首部,否则客户端就认为连接仍为维持在打开状态。如果客户端要建立一个非持久连接,则需要在请求中包含Connection: Close首部;服务器在处理完该事务后,就会在响应中包含Connection: Close首部以告知客户端连接已关闭。如果客户端不想在一条persistent连接上发送更多请求了,就应该在最后一条请求中包含Connection: Close首部。

  • 只要服务器决定在事务处理结束后关闭连接,就必须在响应中包含Connection: Close首部。但不发送Connection: Close首部也并不意味着服务器承诺永远将连接保持在打开状态。同样地,不管连接是否维持在打开状态,或Connection首部取了什么值,客户端和服务器仍然可以随时关闭空闲连接。

Author: bugwz
Link: https://bugwz.com/2017/04/26/web-performance-http2/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.