mengtuifrontend / Blog

芦叶满汀洲,寒沙带浅流。二十年重过南楼。柳下系船犹未稳,能几日,又中秋。 黄鹤断矶头,故人今在否?旧江山浑是新愁。欲买桂花同载酒,终不似,少年游。
18 stars 5 forks source link

漫聊前端缓存 #3

Open mengtuifrontend opened 5 years ago

mengtuifrontend commented 5 years ago

漫聊前端缓存

缓存对于我们前端来说是一个用于提高网页性能非常重要的工具。简单的“缓存”两字,其中包含了许多有趣的知识,其中对于缓存细粒度的讨论也一直不绝于耳。

目前见过的缓存细粒度分为三类:

下面让我们一起看看这三种方案的到底是如何处理生产环境下的缓存的。

以全部文件为最小单位

这种做法相当的粗暴而且历史悠久,无论开发过程中的文件有多少个,整个打包成一个文件后在页面中引用。这样做带来的好处是可以最大的减少对资源的请求数,至今仍使用这种方法进行线上缓存处理的网站还有很多(特别是某些后端框架自带合并资源功能时)。然而这种方案的缺点也十分明显。

由于太粗暴,开发环境中的一个文件被修改,就会导致整个页面的资源都将失效。客户端不得不因为一小部分的修改,而重新下载所有的内容,这是移动互联网大环境下不能被容忍的(流量就是钱,客户也是,公司也是)。

另外一点是对于开发人员来说维护困难。由于每个页面的业务逻辑都不相同,所以每个页面引用的资源文件也不会相同。但是当一个整站共用的文件出现了修改,那么整站的资源内容都要重新打包上传,如果不是某些框架或者工具为你处理的话,相信大家都会疯掉。

改良版本一 —— 一分为二

针对上述两个硬伤,马上出现了方案的改良版。将整站通用的资源打包一份,页面级的资源打包一份。页面上引用的资源数不再是一,而是二了。

改良后的方案,在遇到整站共用的内容发生修改时,仅仅重新打包整站共用资源即可,大大降低了维护成本。而遇到页面级逻辑发生修改时,用户也仅仅是重新下载新的页面级资源,相比以前好得多了。

改良版本二 —— 三层架构

所谓的三层架构,即将资源划分成 通用底层 - 通用组件层 - 页面级逻辑层 三层。(这个叫法我是从一个特别老的前端写的书上看到的...)相比上面一分为二的做法,这种方案将细粒度稍微的多细分了一下,将整站通用资源分为了底层(不含 UI 逻辑)与组件层。这种方案在底层或组件层发生修改时可以更好的为用户节约流量。

以单独文件为最小单位

上面的方案有点奔放,而这种方案就又稍显别致了。将可细分的功能全部做为文件,并通过 hash 命名等手段在页面中引用。一旦一个功能做出修改,用户也仅仅需要下载该功能相关的资源。这对于大公司来说是有其必要性的。主要是因为大公司的网站 PV 量比较大,一个文件的修改都可能会消耗很多 CDN 流量,所以要尽量控制所修改文件的大小。

以字符为最小单位

这种方案是目前看过的最小细粒度的方案,已经到了字符级别了。通过向查询,当有内容发生修改时,服务器仅仅返回包裹修改内容的一些字符串,而并非整个文件。这个响应内容会被客户端接收并重新写入对应的修改处后生成新的本地缓存(基于 localStorage 方案)。

这个方案可以说是别致的让人诧异,最小化的减少因为功能逻辑修改而消耗的流量。但是这个方案的使用缺很少见过。

最早的该方案版本出现于腾讯(更早是不是国外有过就不得而知了),但是据传言该方案在分享后被人抢先申请了专利...

为什么是文件名 + hash

资源文件的命名,也影响到缓存效果。比如都是 a.png,更新前后一个名,那么 CDN 无法知道源站上文件是否是新的,都会向客户端返回节点命中的缓存。

现如今,基本上资源名都是以 文件名 + hash 的形式命名,这是历史发展的结果。

最早之前,前端工程化十分薄弱,需要依赖 RubyPHP 或者 JAVA 等后端语言进行工程化处理,所以在资源的使用上,刀耕火种的年代一般都是通过 URL 带上时间戳或者版本号参数进行资源更新。

这种使用版本号/时间戳更新资源的方式的问题很多,由于版本号/时间戳都通过打包生成,那么一个文件修改就会升级版本号/时间戳,这会导致无需更新的文件也被强迫失效,CDN 资源被浪费。

版本号/时间戳还有可能被放置在文件名 中,但也是换汤不换药(自然不同的形式有不同的特点,下面会讲到),依旧难以解决这个问题。

<img src="path/name.jpg?v=1.2.3" />
<img src="path/name.1.2.3.jpg" />
<!-- 升级成 -->
<img src="path/name.jpg?v=1.2.4" />
<img src="path/name.1.2.4.jpg" />

后来就以文件的摘要作为版本号,就解决这个问题了,因为每个文件都是以自身内容的摘要进行版本管理。

所以还是有两种使用方案,URL 参数和文件名:

<img src="path/name.jpg?v=hash_code" /> <!-- 第一种方案 -->
<img src="path/name.hash_code.jpg" /> <!-- 第二种方案 -->

第一种是摘要放在 URL 参数中的方案,由于 CDN 的回源机制,这种方案最起码的问题是不通用。

很多 CDN 回源机制会加入被动防御机制,即是不考虑 URL 参数的,也就是

<img src="path/name.jpg?v=hash_code1" />
<img src="path/name.jpg?v=hash_code2" />

这两个请求都是同一个命中,这可以确保 CDN 不被恶意构造的请求刷流量或者程序错误导致的 CDN 长时间失效(比如:path/name.jpg?v=${Date.now()})。

第一种方案还会遇到同名文件覆盖后回退版本、同名文件更新提前被访问等问题,这里不多赘述。

第二种方案在文件名中带入摘要可以很好的解决上述所有问题。

  1. 资源版本仅限于自身,和其他资源无关;
  2. 发布时不会覆盖原有文件,回滚操作无需进行资源文件的回滚;
  3. 更新 HTML 之前可以大胆放心地更新资源文件不必担心上线被提前访问;

总结

还有将版本号放入 URL path 的方案,但这方案太小众,而且和目前流行的方案相比也没什么优势,弃之不谈。

待到 HTTP2.0 中的 service push 功能能够优雅的使用的时候,届时前端资源缓存方案都将会迎来一波革新。


Thanks