🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
## 前言 经过前面两篇文章,相信你已经能从一些方面进行网页的优化,下面从其他角度分享一些常用的策略。 一个页面,会经历过加载资源,执行脚本,渲染界面的过程。我们知道,100ms对于计算机来说,可以干很多事情了,但是对于网络请求,可能一次RTT就没了。因此,页面加载对于Web性能是重中之重。 ## 加载性能分析 加载的快慢可以总结成受两个因素影响:阻塞与延迟。 1、阻塞。浏览器在解析到脚本时,会阻塞页面,等到脚本下载执行完才继续解析文档。此外,浏览器还会限制同域下的并行请求数,超过这个限制后的请求就会被阻塞住。 2、延迟。网络请求都不可避免会有延迟,网页上的延迟有两种,一是DNS查询,二是TCP连接。 * 克服这些缺点,我们有一些约定俗成的方案: * 静态资源要支持304,开启HTTP缓存控制 * 开启gzip,压缩HTTP body * css放在html的head里,js在body底部 * 合并请求 * 使用雪碧图 * 域名分区(突破并行限制,也避免传输过多cookie) * 使用cdn 3、 加载过程图解 定向--dns查询--建立tcp连接--下载dom--页面根据样式和脚本渲染--交互流动性 ![](https://box.kancloud.cn/d51bfa1690a8711f481f68edf214f1e9_640x78.png) ## 常见的解决方案 ### 避免重定向 重定向意味着要重新发起请求,当然我们没事也不会乱跳。这里要说的一种重定向是,访问HTTP站点,跳转到HTTPS。 避免这种跳转,我们可以用**HSTS策略**,就是告诉浏览器,以后访问我这个站点,必须用HTTPS协议来访问,让浏览器帮忙做转换,而不是请求到了服务器后,才知道要转换。只需要在响应头部加上 `Strict-Transport-Security: max-age=31536000` ### 预加载 DNS查询需要个RTT时间,在浏览器级别,系统级别都会有层DNS缓存,之前解析过的可以直接从本机缓存获取,以减少延迟。 Web标准提供了一种DNS预解析技术,因为服务器是知道页面即将会发生哪些请求的,那我们可以在页面顶部,插入 <link rel="dns-prefetch" href="//host/">,让浏览器先解析一下这个域名。那么,后续扫到同域的请求,就可以直接从DNS缓存获取了。 此外,Web标准也提供prefetch,prerender的预加载技术。prefectch会在浏览器空闲的时候,向所提供的链接发起请求,而prerender不仅会请求,还会帮你在后台渲染页面。如果在一个页面中,你知道用户有很大概率去点某个链接,可以尝试把这个链接加到prefetch或prerender,那么用户就会秒开这个页面了。 ### 使用TCP、TLS最佳实践 HTTP请求要经过建立TCP连接这一步,而TCP为了可靠传输,建立连接需要三次握手。如果网站又接入了HTTPS,那还要额外多两次RTT时间以建立安全通道,这样耗费了很多时间。HTTP是建立在TCP、TLS之上,那么TCP的最佳实践,SSL的优化都是适用于HTTP的优化。 比如TCP慢启动过程非常影响性能的,我们可以把初始窗口调大,让慢启动更快。对于TLS可用缓存session_ticket之类的优化可以减少一次RTT。 ### 内联 对于一些简单的页面,CSS样式和JavaScript脚本甚至图片,可以不必使用外联的方式引入,直接把子资源内嵌到HTML里,图片可以用base64编码内嵌,这相当于请求页面时,服务器顺便把子资源给一共推送过去了。传输的内容都一样,但减少好多请求了,自然节省不少时间。 不过这样做的缺点是浏览器无法缓存这些子资源,这种做法只能降低首次加载时间,所以需要看取舍了。可能比较适用于一次性的页面,类似活动之类的。 ### 手动管理缓存 为了代码架构清晰,便于维护,我们都会用模块化的方式去编码,每个模块一个文件,这样带来的问题是一个页面需要很多文件,要很多请求,这对页面性能是不利的。合并是解决这个问题的好方法,但又因为HTTP缓存机制是基于URL的,只要某个模块一改动,整个合并资源都要重新下载。 在对性能要求较高,比如在移动设备环境上,我们可以利用HTML5中的localStorage特性,来实现手动控制缓存。大概的思路是,在定义模块时,同时将模块的代码和版本号分别储存到localStorage,在下一次打算请求模块之前,我们先判断模块的最新版本是不是在localStorage中,将不存在的模块组合在一起,请求动态合并的资源。 不过,这种方案可能会引发安全问题。假如同域下的其他页面被XSS攻击,坏人就可以篡改localStorage的内容,可能导致原来的页面代码被植入恶意程序。解决的方法是,在执行模块之前,算一下代码摘要,对比下服务器给的该模块的摘要,再决定是否使用。也可以使用SRI策略,由浏览器帮你做校验。 ### HTTP持久连接 TTP持久连接可以重用已建立的TCP连接,减少三次握手的RTT延迟。浏览器在请求时带上 `connection: keep-alive` 的头部,服务器收到后就要发送完响应后保持连接一段时间,浏览器在下一次对该服务器的请求时,就可以直接拿来用。 以往,浏览器判断响应数据是否接收完毕,是看连接是否关闭。在使用持久连接后,就不能这样了,这就要求服务器对持久连接的响应头部一定要返回content-length标识body的长度,供浏览器判断界限。有时,content-length的方法并不是太准确,也可以使用 `Transfer-Encoding: chunked `头部发送一串一串的数据,最后由长度为0的chunked标识结束。 ![http持久链接](https://box.kancloud.cn/ba39c8c55819f52c93dd26d39d30a559_450x280.png) ### HTTP管线化 HTTP管线化可以克服同域并行请求限制带来的阻塞,它是建立在持久连接之上,是把所有请求一并发给服务器,但是服务器需要按照顺序一个一个响应,而不是等到一个响应回来才能发下一个请求,这样就节省了很多请求到服务器的时间。不过,HTTP管线化仍旧有阻塞的问题,若上一响应迟迟不回,后面的响应都会被阻塞到。 ![http管线化](https://box.kancloud.cn/ee814167106426870e6d97998f645744_450x280.png) ### bigpipe 目前大部分应用是吧界面以及数据全部准备好之后再进行渲染,而实际上是可以按照分帧分别处理的,如果页面包含多个较独立部分,也可以每处理完一部分就马上输出,这样可以缩短白屏。从用户感受上可能会更好,页面上一直有所反应,而不是一直白屏,完全不知道你在干嘛。 各种各样的优化,都在填HTTP/1.x留下的坑,HTTP/2带着填坑的使命,从根本上去解决这些问题。HTTP/1.x是一个文本协议,这注定它是非常冗余的协议,HTTP/2改变了这一点,在HTTP/1.x的语义上,将文本数据封装在帧里,并采用二进制编码。 HTTP/2的性能怎样,akamai的这个demo(https://http2.akamai.com/demo)估计会让你很兴奋。 ## 服务器推送 作为HTTP/2的一个重磅新功能,我们不要简单理解字面意思,其实不是你想推,想推就能推的,服务器要遵循请求-响应这个模型,只不过服务器对同一请求可以推送多个响应。客户端在交换 SETTINGS 帧时,设置字段 SETTINGS_ENABLE_PUSH(0x2) 为1显式允许服务器推送。 在HTTP/1.x时代,其实我们已经体验过了“服务器推送”,就是资源内嵌到HTML里。服务器在响应HTML时,就已经知道浏览器会请求哪些子资源了,这时一并响应这些子资源,可以节省了服务器到浏览器以及浏览器解析再发请求的这段延迟。但是内联的问题是浏览器不会缓存这些数据,这意味要浪费很多流量,而且有缓存时网页性能还是很好的。 服务器推送解决了这个问题。服务器在接受到请求时,分析出要推送的资源,先发个 PUSH_PROMISE 帧给浏览器。此帧包含一个新的流ID,还有header block fragment字段,内容是请求的头部信息,可理解为服务器模拟浏览器发起请求,然后再发送各个response header和response body。浏览器收到 PUSH_PROMISE 帧时,根据header block fragment字段里的url,可以知道当前有没有缓存,从而判断是否要接收。如果不要,浏览器就要发送个 RST_STREAM 来终止服务器推送。 如果浏览器不要这个推送,就会出现浪费流量的现象,因为整个过程都是异步的,在服务器接收到RST_STREAM时,响应很有可能部份发出或者全部发出了。这种情况只能视场景而定,若是流量浪费不能容忍,我们可以使用prefetch来替代,让浏览器尽早发现需要的资源,而HTTP/2中创建新的请求并不需要多少时间,所以大概多了个RTT的时间。