于是关于网站性能优化都能做些什么

于是关于网站性能优化都能做些什么

2021年02月08日

那预期只要是为网站的最终显示出了一份力的地方都可以考虑一波优化了。只是没有数据支持的强行优化应该算耍流氓吧

尝试总结一波网站性能优化

从经典面试题开始:

在浏览器地址栏输入网站地址并回车,之后都发生了什么

这里做个简化,所有请求都是HTTPS协议, HTTPS安全性高于HTTP,并且配合h2之后的性能提升完全可以cover证书加密解密带来的开销(HTTP/1.x没有TLS)。

预期答案:

  1. 域名 -> DNS(Domain Name Server) -> 浏览器获得服务端对应IP(DNS Lookup)
  2. 向对应IP发请求获取数据(此处记为index.html)
  3. 浏览器解析index.html并开始加载index.html中需要的各种资源, 页面开始渲染

1 步骤1优化思路

此步骤直接影响页面的打开速度

这里考虑的点在于: 浏览器与服务端建立连接之前需不需要进行域名解析, 如果需要,这次域名解析需要多久。

对于接入了CDN的网站,基本没什么可以优化的了(CDN服务商本身会做这些优化, 或许可以考虑源站到CDN节点这段距离的优化)
如果没有接入CDN,比如本博客(起码写这个总结的时候还没有), 简单的云服务器上跑Nginx服务, 还是有可以优化的点的。

1.1 根据DNS服务器的运作原理, 客户端和服务器地理上距离越近, DNS解析越快。

举个例子,本博客是用的腾讯云四川地区的云服务器,那么在四川的用户通过本地DNS服务器找到域名www.writeyoursmile.com对应IP的速度相比其他地区就会比较快。

这种情况只对特定情况有用毕竟大部分网站的用户都是遍布全国的。
除此之外解析速度也和DNS提供商的服务质量有关系。

1.2 合理设置域名解析的TTL

TTL(Time to Live) 意思是DNS解析成功之后缓存的时间。如果设置的比较短,比如1秒,那么基本每次请求某个域名,浏览器/客户端都需要重新向DNS发起域名解析的请求。

这种情况下可以根据业务需要设定一个合理的TTL。
比如本博客基本就种在这个服务器上了,ip也是固定不变的,于是设置了一个相对很长的TTL,604800s(一周)。
那么浏览器在第一次域名解析成功之后的一周内,都不会再向DNS重新请求解析了,浏览器之后打开页面直接走本地DNS缓存拿到SIP。
设置太长的TTL问题不大,前提是考虑将来某天突然换IP的代价, 比如考虑先把TTL设置成短时间,然后等各处缓存过期之后再换。

1.3 小结:上CDN吧
1.4 参考资料
  1. 3 TIPS FOR IMPROVING YOUR WEBSITE DNS PERFORMANCE DURING THE HOLIDAYS
  2. Global Server Load Balancing
  3. What is DNS TTL + Best Practices

2 步骤2优化思路

  1. 向对应IP发请求获取数据(此处记为index.html)

这一步是浏览器拿到服务端IP(姑且称SIP)之后,开始通过HTTPS请求获取index.html

展开细节就是向SIP的443端口建立TCP连接,连接建立之后开始TLS的握手(期间包括证书的发送和验证),再然后就是应用数据(在这里就是index.html)的传输了。

同步骤1,接入了CDN之后,这些优化已经不是自己可以控制了。如果没接入CDN,那这一部分可以继续看下去。

2.1 TFO(TCP Fast Open)

TFO起作用的时机是:在之前建立过TCP连接然后断开之后,如果又要建立TCP连接,那么第二次以及以后的连接建立可以跳过TCP的三次握手。 (此机制对h2来说似乎并没有显著帮助,因为h2只建立一个TCP连接,在访问网站期间基本不断开了)

2.2 第一次请求结果大小14Kb以内

这个指的是index.html的大小,如果是vue/react之类的webapp那么基本还是都是可以做到的。
因为TCP Slow Start这个现象的存在,初始返回的response如果不能控制在14Kb以内,将会分成多个TCP包传回。
本博客用的是hexo, 14Kb基本不用考虑了。

2.3 OCSP(Online Certificate Status Protocol) Stapling

在TLS握手的之前客户端会检查证书有效性,OCSP则是用来查询某个证书是否过期了或者被吊销了。此时有两种情况,一是客户端之前查询过,那么会有缓存,直接拿过来上次的查询结果决定要不要继续建立连接;另一种情况是客户端没查过,那么需要等客户端临时通过OCSP查询来得到结果,然后继续, 如果OCSP查询超时,客户端可能会默认证书有效。
OCSP缓存的时间一般比较长,一天到一个月的都有,由OCSP服务的提供商决定。
OCSP Stapling则是服务端将这个OCSP的查询结果缓存在服务器本地,然后在TLS握手期间将结果给到客户端进行检查,于是客户端就不用自己去发送请求查询了。

检查某网站有没有开启OCSP Stapling, https://www.digicert.com/help,或者https://www.ssllabs.com/ssltest/

2.4 TLS1.3

TLS1.2完成握手需要两个RTT(Round Trip Time, 客户端发起请求到收到服务端回应的时间算一个RTT), 而TLS1.3需要一个RTT。

本博客按照参考4尝试了一次,将openssl和nginx都重新编译安装了一遍,然而不知道坑在哪里目前位置还是只能用TLS1.2,残念。

2.5 压缩

如果只是一个index.html,特别还是用了前端框架的,基本上就小得可怜, 压缩了也没多大意义。

2.6 HSTS

HSTS(HTTP Strict Transport Security), HTTP严格传输安全。
作用场景为: 服务器在支持HTTPS服务的同时也支持了HTTP服务,只是将HTTP请求全部重定向到对应的HTTPS服务。
优势为: 浏览器将不会去向服务端请求然后等服务端返回301或者类似的重定向响应,而是直接自己构造一个307 Internal Redirect请求,然后浏览器下一步直接访问对应的HTTPS服务。
开启方式为:在HTTPS服务的请求响应中加入对应的header。 比如在nginx中的配置如下

1
2
3
4
5
6
7
server {
#...other config
location / {
root /path/to/res;
add_header strict-transport-security "max-age=63072000; includeSubDomains; preload"; #HSTS
}
}

开启HSTS对性能提升比较局限,更多还是体现在安全性上。
检查是否成功开启了HSTS,可以在https://hstspreload.org检查

2.7 小结: 在CDN面前,其他优化都往后稍稍
2.8 参考资料
  1. TCP Fast OPEN – Citrix NetScaler
  2. Monitor certificate status with OCSP
  3. Enable OCSP Stapling on Your Server
  4. How to enable TLS 1.3 on Nginx with OpenSSL Centos 7
  5. Why websites should be using HSTS to improve security and SEO

3 步骤3优化思路

  1. 浏览器解析index.html并开始加载index.html中需要的各种资源, 页面开始渲染

这部分优化思路大概可以总结成,能早点开始的就早点开始,能延后加载的都尽量延后加载。
就很像为了减少开机时间,把开启自启能禁用都禁用,犹不满足于是将需要自启动的服务改成延后启动。当然太过分了体验也会比较差,开机时间也只是其中一个指标而已。

下图为https://www.writeyoursmile.com/打开之后的一些记录监控。
ygQMJs.md.png

资源先放入浏览器资源下载队列并不意味着可以先开始下载,这个要根据资源本身的优先级决定。
当加载并解析完index.html, 其中涉及的外部样式表、样式表、图片、Javascript脚本等文件会根据出现在index.html中的位置加入下载队列。关于优先级的设定细节移步看参考资料。

3.1 dns-prefetch 和 preconnect

作用场景为: 使用了cdn资源并且对应域名不是当前页面的域名。
使用方式:

1
2
<link rel="preconnect" href="https://fonts.gstatic.com/" crossorigin>
<link rel="dns-prefetch" href="https://fonts.gstatic.com/">

其中dns-prefetch会让浏览器提前开始dns的解析,而不是等到准备下载具体资源的时候才开始。
preconnect则更除了提前dns解析,还会提前建立TCP连接和TLS握手。注意不能过度使用preconnect。

3.2 prefetch 和 preload

浏览器对这两种方式标记的资源处理方式基本一致, 区别在于preload的资源下载优先级比较高。而prefetch的资源下载优先级最低。
处理方式: 下载对应资源并放入缓存, 在将来用到的时候可以从缓存中拿到。
使用场景: preload对应的资源一般会在当前页面用到, 而prefetch对应的资源则可能再下个页面或者比较靠后的时间才会使用。

3.3 defer 和 async

这两个属性用于script标签上,都是下载脚本的时候不阻塞DOM的构建,区别在于async是下载完立刻开始执行,此时会阻塞, defer则会在DOM构建完毕后,DOMContentLoaded触发之前执行完毕。
比如本博客接入的评论功能的js脚本可以defer安排上。
使用效果: 不是很明显, 因为基本上js脚本都会建议放在body标签内部的最后位置。 这个地方再下载或者执行脚本虽然说会阻塞DOM构建,但其实DOM基本已经构建完事了。
如果有必须放在header部分而且并不是很重要的js脚本,则可以考虑用async或者defer来避免阻塞DOM构建。

3.4 延后加载css

这里没有defer可用,如果首页某些css用不到的话的话甚至不需要参与一开始CSSOM的构建,可以更快的将第一屏展示出来。
实现思路:后续使用js脚本动态往head标签里边插入link标签(webpack)。
另外如果网站是响应式,可以考虑将不同设备的样式分开文件来写,然后在引入的时候使用media属性来加以区分,浏览器会根据media属性来做选择性加载或者做优先级的调整。

3.5 压缩

代码压缩: 关键词js uglify, css minify。
文件压缩: gzip, brotli。 开了压缩之后相对不开省流量, 减少了网络传输时间。 客户端需要额外的时间来解压缩,自行权衡。压一压文本还行, 图片就别开压缩了,本身就是压缩格式。另外很小的文件就没必要压缩了,可能压完之后反而变大。

3.6 文件缓存

除了静态资源的缓存,甚至不常做数据更新的接口都可以缓存。 在http2开启之后多个小文件已经不是性能瓶颈,更方便做增量更新, 这里写太长了,将来想总结还是分开吧。

3.7 小结

除了压缩这块是交给CDN的(估计能够在CDN控制台配置),其他的都是开发中可以控制的。总体可以在Chrome的LightHouse指导下优化。

3.8 参考资料
  1. Populating the page: how browsers work
  2. 浏览器页面资源加载过程与优化
  3. Chrome Resource Priorities and Scheduling
  4. First Contentful Paint (FCP)
  5. dns-prefetch
  6. HTML Living Standard