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

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