网站访问速度优化

先增加限定条件

印象中,曾经一段时间,有个议题『从输入URL到页面加载发生了什么』被不少人讨论。
这是一个很好的问题,也是一个很糟糕的问题,假设退一万步我要去面试讨生活时,有人提这个问题,那么『答案』是要收费的;而且,即使收费了,也无法保证全面性,甚至也仍然混杂了一些认知错误。
这个问题,就好像在问 『我因何于此处』,宏观得以至于企及了哲学的命题。

因此,我们要限定条件,本节的内容,主要指 HTML 页面的简单渲染,连浏览器的离线缓存 (比如 LocalStorage)都不涉及。

服务器端的速度优化

在服务器端,优化速度,其实没有太多的诀窍,就是一些基本的方法。
一般来说,当一个页面从请求发出到最后载入完成,超过了 1 秒 (也就是 1000 毫秒,亦为 1000ms),人在知觉上就会产生『慢』的感觉。

一般可以使用国内线路的服务器,但需要备案,手续上比较麻烦。国内的线路,从客户端请求出去,再回来,一般也就 30ms 左右;如果服务器在美国,则是 400ms 左右了。而且几乎所有国外的服务器,都存在线路抖动、拥堵的问题,相同时间不同地点、相同地点不同时间,线路质量都不一定完全相同;但也不要因此危言耸听,绝大部分时间,可靠的服务器机房,其国内外线路的质量可以算稳定的。
然后,确保足够的带宽,比如只有 1Mb 的带宽,那么图片就用 CDN,不要直接走服务器的带宽,不然一个页面打开带宽就完全堵死了。还有比如说号称 1G 的线路接入,但实际上,作为网站而言,实际能承载的带宽可能只有 100Mb 甚至更低,因为主要考虑的是 Web 服务器的上行速率 (也就是服务器传出到浏览器接收的速率),而不是下行速率。
如有可能就开启 gzip,HTML 是超文本,gzip 就像压缩 zip 文件一样会压缩 HTML 页面,然后浏览器会自动解压,这样就节省了传输的时间,虽然会消耗服务器以及客户端浏览器的 CPU,但这个性能损耗微乎其微,就是拿性能换时间了。传输的速度 才是真正意义上的网速,但一般情况下,它的速度很快,毕竟页面本身是普通文本而已,很小,可能也就几个 KB,不像一张图片,通常就要几百个 KB 了。

综上而言,我们要保证在 1 秒内完成页面的载入,假设服务器又在美国,因为物理距离产生的延时,那么基本上 500ms 的基础时间,已经没了,而留给我们只剩 500ms 了。
至关重要的是页面的生成时间,对于静态页面来说,这个时间微乎其微,而动态页面则在多数时候,只要后端的页面代码不要犯大的错误,多数在 300ms 内就能生成了。但通常也有不少人犯错,对数据库有太多的(低效率)查询,页面生成的时候,又有太多的遍历、太多的逻辑操作;特别是新手的时候,一不小心,生成页面的过程就直接破秒了。
我们在页面生成的时候,还会做进一步的优化,就是 缓存,这个缓存基本上就是内存上的缓存 (简单比如使用 Memcache),命中缓存后,可以让页面生成的速度达到 1ms ~ 10ms 之间。
好的缓存设计,是可以感觉到不到缓存的存在的,基本上可以让动态的页面,其性能接近静态页面。但也并不是很容易的事情,技术的实施,要考虑实际的成本,甚至要考虑到团队内可能明显存在技术瓶颈的现实问题。

简而言之,1 秒的速度是个边界,超过了,会明显感觉到速度慢。
如果线路在国内,处置得当,一般 100ms (也就是 0.1 秒) 就足够足够呈现一个页面了;实际的感觉,就是飞快的飞快。

HTML 页面优化

由于前端技术在这些年的快速发展,现在很多网站的页面生成,并非由原始的 HTML 构建的,而是由 Javascript 的框架完成渲染。特别是在移动端倾向的时代里,很多移动端 App 本身的主要代码也是用 Javascript 完成的。
在这些背景下,早些年非常强调的一些优化原则,则被忽略了,当然了,『原则』本身也是用来打破的。

我简单将自己会遵循的原则罗列一下,有些时候因为特定的原因不必完全遵循,所以,这也仅作为参考:

  1. 如无必要,不引入框架,可以减少脚本资源的请求数以及流量;
  2. CSS 引用放在 HTML 源码头部,Javascript 的引用,放在 HTML 源码的尾部 (避免 Javascript 前置加载影响页面渲染);
  3. 图片资源,务必控制尺寸 (width&height)、大小 (size)。

其它的,就不苛求了。
严苛的话,CSS、Javascript 文件中的空格都会去掉,以减少前端资源文件的大小;甚至会直接将多个 CSS 、Javascript 文件,按文件类型合并成一个,以减少页面内前端资源文件的请求数。
而技术总是在持续性改善着,比如说 HTTP/2,很多人可能不知道它是什么,但它作为 HTTP/1.1 的升级版,潜移默化地提升着我们日常的网页访问速度,像多个 Javascript 文件的合并,在 HTTP/2 下的意义就不大了。

另外一点很重要,就是要 优化图片,可以使用 ImageOptim.app 这个 App。
我们相机、手机直出的照片,除了有 EXIF 隐私信息之外,在 Web 端,更主要的问题是尺寸太大了。一个页面如果 100 张照片,每张 5Mb 大小,那就是 500Mb;这就恐怖了,一个页面要载入 500Mb ……
考虑到访客终端设备的屏幕,一张图片压缩优化之后,JPG 的质量 80% 左右也不太影响实际视觉,但整体图片的大小就降下来了,一般几百 KB 也就够了,而不是动则几个 MB。
页面中内容性质的图片,我们一般需要在后端进行控制,当它被浏览的时候,会自动出图,各个参数调整到比较合适的情况,以加快网页中显示图片的速度。当然,还可以通过 Javascript 来优化,这里有个术语叫 lazyload,可以自行再了解。
还有 CSS 中可能要用到的素材,比如背景图,都要自己处置好优化,作为素材,这些一般我们自己手工处理下便可;或者说,如果不是真正地表达了一种视觉上的美感而只是一般性的修饰,我们应该尽可能避免在 CSS 中使用图片作为素材。

Gzip

页面优化,对于启用 gzip 之后,CSS、Javascript 文件中有空格其实就已经是无所谓的事情了。
我们便就用代码的形式,对 gzip 的基本机制,感知一下吧:

>>> from io import BytesIO
>>> from gzip import GzipFile
>>> 
>>> def gzip_content(content):
...     zbuf = BytesIO()
...     zfile = GzipFile(mode='wb', compresslevel=6, fileobj=zbuf)
...     zfile.write(content)
...     zfile.close()
...     return zbuf.getvalue()
... 
>>> a = ' ' * 10000
>>> print(len(a))
10000
>>> a2 = a * 100
>>> print(len(a2))
1000000
>>> b = gzip_content(a)
>>> print(len(b))
46
>>> b2 = gzip_content(a2)
>>> print(len(b2))
1004

a 是一个 1w 长度的空字符串,a2 则是 100w 长度的字符串,两者压缩后,压缩比都很高,如果将 a2 再扩大 100 倍,得到的压缩比倒不是线性变化。此处的重点是文本压缩后,一般都是高压缩比的,再去省一两个 KB 的价值就显得非常有限了,这也是上文提到的,通过去掉 JS/CSS 文件中的空字符来缩小文件尺寸,其实意义是相对有限的。

说到压缩比,再加上我们在讲解『动态页面』的过程中,有提到: 不要相信用户的输入
除了 gzip 之外,还有我们更常见的 zip 压缩算法,可以尝试自己去搜索一个关键词: Zip 炸弹
一个 KB 级别的 zip 文件,解压缩后可以成为 PB (1PB=1024GB,1GB=1024MB) 级别的。假设后端需要去处理用户传入的 zip 文件,然后解压缩,且无防范措施;那么,Zip 炸弹,一炸一个准,直接把你的服务器给『炸』(这个词性并非物理意义上的爆炸)了。