如何提高网站的加载速度?

2018-03-30 08:00

我在优步工作遇到了一个很窘迫的情况,口口声声说要给用户提供最安全、最流畅的体验,却突然发现我们几乎所有的网站在手机 GPRS 的网络环境下,竟然要加载整整两分钟!简直是丧心病狂!

想来其实也并不奇怪啦,硅谷的工程师都被公司提供的最快的网络和最先进的设备惯坏了。我老板有次跟大家瞎聊,说绝对不要相信公司的网络和设备,有个脸书的工程师在离开脸书后,因为什么缘故要跟公司打官司,公司搜集了一下他过往的历史记录,说,诺,你当年可是在公司网络下载过盗版 A 片哟,你要跟我们打官司的话,我们就把细节公开出来。呵呵,大家问为什么要去公司下载呢?——可能是因为网速快吧🤣,下载几十 G 的东西只要几秒钟……

言归正传,想要让什么东西传输得飞快,其实只需要很简单的两个黄金法则:减少延迟,降低负载 (minimizing 1) latency 2) payload)。

下面就是具体的一些做法。

# 1. 减少延迟

减少 DNS 查询时间 首先,你可以选择一个快的 DNS 提供商,一般来讲 Cloudflare 快于 DNS Made Easy 快于 AWS Route 53 快于 GoDaddy 快于 NameCheap,尽管不同地区当然实际情况会不同。其次,要利用 DNS 缓存,这需要在“性能”和”保持最新“两者中做出权衡。然后,现代网页中会有很多发向第三方的 XHR,比如 Google Analytics,要意识到这都会增加额外的域名解析。最后,可以使用“DNS 预读取”的技术,比如在 HTML 中加上 <link rel="dns-prefetch" href="//www.example.com/" >

复用 TCP 链接 打开和关闭资源都是有额外开销的,所以我们要尽量复用一个需要反复使用的资源,在这里就是多用 HTTP KeepAlive

尽量不要重定向(redirect) 这个好理解,每多一个来回就多一个延迟。就是要注意不要有环环相套的重定向链,比如 http://www.example.com -> http://example.com -> https://example.com -> web service 这种。

上 CDN,把资源提前传到离用户近的地方,比如网飞虽然使用 AWS,但是公司为了保证效果,甚至自己开发硬件,直接和本地的 ISP 提供商合作部署资源。

干掉没必要的资源 延迟加载,各种资源暂时不用就不要加载,善用一些动态加载 JS 的库。比如,你的网站要用到谷歌地图,但是不要所有的页面都有谷歌 API,只有当需要用到的时候,才去加载。有时候我们可以在 build 的时候分割 (code splitting) 自己的 JS bundle,然后动态加载。分割的方法有很多,其中,根据路由来分割就是一种不错的选择。

**利用浏览器的缓存**所谓缓存本质上通常是用空间换时间,比如,先前说的 CDN 把资源放到离使用者近的地方。又比如,用贵的媒介——内存、SSD换便宜的媒介——硬盘、磁带。还比如,用访问更快更简单的数据结构——O(1) 的 hashtable 换更慢更复杂的数据结构——O(log n) 的 B/B+ tree。缓存的学问很多,这里单讲浏览器的缓存,主要是由如下两种 HTTP headers 控制。人们常常疏忽,导致只设置了其中一种。

  1. 缓存头,包括 cache-control: max-age 和 expires,如果两者都存在的话,以 max-age 为准。对于 single-page application (SPA) 而言,要记得在 JS bundle 的文件名中加 hash,以保证每次部署之后都能拿到最新的代码
  2. 验证头,比如基于时间的 last-modified 和基于内容的 ETag 。前者应该比较少用,毕竟服务器本身是无状态的,很难去动态记录页面改变的时间。这种动态的信息就更难放到反向代理这种相对静态的地方了。相比之下后者根据 response 产生哈希值应该更可行。

压缩资源,比如采用压缩比更高的图片格式,用 JPEG 和 WebP,不用 PNG。而 HTTP2 会自动压缩 HTTP 消息头。Nginx 能自动帮我们压缩 response。CloudFlare 也会自动帮我们压缩。

# 2. 降低负载

干掉没必要的负载 尤其是 Cookie。虽然 HTTP 标准并没有明确规定 header 和 cookie 的最大尺寸,一般浏览器和服务器会限制 cookie 4KB,header 8KB ~ 16KB。 但是毕竟每个请求都会自动带上它,所以不要滥用 cookie。

并行请求和处理资源,多用异步非阻塞(async/nonblocking),少用同步阻塞(sync/blocking)。使用预加载这种技术理论上能够让性能提高大约 20%。多复用连接(keepalive),多利用并行的连接,具体我们下面针对 HTTP 1.X 来讲。

# 3. 对传输协议有针对性地优化

HTTP 1.X 浏览器开连接的特性是,每个 origin (origin=scheme+host+port) 只能开 6 个连接。所以,对于单个连接,使用 HTTP keep-alive 复用连接,还要把零碎的资源打包或者 inline 以减少总连接的数量。对于并行,使用域名分片 (domain sharding),把资源分散到更多的域名,但是注意这样会引入更多的 DNS 查询。

HTTP 2.X 引入了大量的优化技术,每一个 origin 只需要一个多路复用的连接,这样我们就不需要像 HTTP 1.X 那样做域名分片了;粗粒度的打包的资源也没有必要了,因为没法做到动态延迟加载,也很难有针对性地集中优化和压缩资源。某些资源,如果我们知道浏览器一定会加载,我们可以用 server push 技术直接往浏览器推,不需要一整个来回。

# 结语

以这些优化原则为基础,我们给项目设计了新的构架,再加上我们换用了一些小巧的框架和库,比如 preactstyletron,最后达到了三十倍数的效果。印度和南美用户对新产品反馈良好,任务圆满达成!

最后推荐一些有用的工具和参考资料。

# 工具

# 参考资料

© 2010-2018 Tian
Built with ❤️ in San Francisco