静态资源(JS/CSS)存储在localStorage有什么缺点?为什么没有被广泛应用?

移动端完全没有兼容问题,只要写版本更新判断就行。 PC端支持到IE8,不支持则请求。 目前一个新项目准备这样架构,但隐隐担忧有什么致命缺点,因为现在还没有被广泛应用。
关注者
1052
被浏览
51683

32 个回答

谢邀。经常用,所以过来回答一下。

我的看法是:PC上用的价值不大,移动端单页面应用(也有叫webapp)值得尝试。

这里要首先提出一个关于静态资源管理和SEO(搜索引擎优化)方面的关联问题:如果要做SEO,那么CSS必然不能进行LS(localstorage)的本地缓存优化。这个原因很简单:

要进行SEO,必须直接输出完整HTML,因此必须让样式在头部以link标签加载。如果先输出HTML,后用js从本地缓存读取样式再插入,会出现严重的阻塞和闪烁问题,相信正常人是不会这么干的。

然后再更正一件事,就是取出localstorage的代码不一定要eval,eval很evil,一个eval函数很有可能影响整个js文件的压缩(出现eval之后不能对变量名进行替换),当然,我们可以通过一些hack避免这种压缩问题,不过我喜欢这样搞:
var script = document.createElement('script');
var code = '!function(){' + getCodeFromLocalStorage() + '\n}();';
script.appendChild(document.createTextNode(code));
document.head.appendChild(script);
没测过效率,应该跟eval没多大差别,真正的性能损耗还是在LS的读取上。

再来解答一个困惑:相比浏览器原生的缓存,LS还有什么优势呢?本来,最棒的浏览器缓存是本地强缓存,我在另外一个知乎答案中解释过本地强缓存的终极用法:大公司里怎样开发和部署前端代码? - 张云龙的回答 工程化实践起来有一定的难度,我也在那篇回答里提到了,用户主动触发的页面刷新行为(比如刷新按钮、右键刷新、F5等),会导致浏览器放弃本地缓存,使用协商缓存(304缓存),用了LS之后,可以完全避免这种情况,等效于无视用户主动刷新行为的本地强缓存。当LS+eval速度大于304协商速度时,LS方案具有统计上的正收益。

此外,讨论缓存问题不能单看一次读取,要从整个缓存的生命周期观察。浏览器缓存以url为单位,一个url可能对应多个文件的打包,N个文件合并成一个url,假设每个文件更新的概率是P,那么整个url缓存失效的概率就是1-(1-P)^{N} ,随着合并文件的增多,每次上线url缓存失效的可能性会非常高,而如果采用LS,配合combo服务,加上精细到文件甚至字符级别的缓存控制,就能让版本迭代过程中缓存的命中率大大提高。

还有,缓存问题也绝不是一个页面的问题,网站很多页面之间会跳转访问,彼此之间也有共享的静态资源,基于url的缓存让跨页面之间缓存共享问题变得粗粒度。举个例子,有A、B两个页面,彼此有访问路径(比如百度首页和搜索结果页之间的访问),其中:
  • A页面使用资源:a, b, c, d
  • B页面使用资源:a, b, c, e, f
假设不考虑并发请求的优化,我们希望尽可能的打包,再假设A页面是主要入口,那么,最合理的方案可能就是a-b-c-d打包(设为[abcd]),e-f打包(设为[ef]),从而使得:
  • A页面使用url:[abcd]
  • B页面使用url:[abcd]+[ef]
由于用户大多首次访问A页面,然后会跳转到B页面,所以访问A页面会很快,再跳转到B页面可以从缓存中使用[abcd]包,再只需加载[ef]包即可。为了更大的缓存利用率,我们让B页面复用A页面的url缓存,但多了一个不需要的d资源这也是合理的。也就是说,基于url的缓存利用可能在有些情况下会资源的冗余加载。想想那些通过url直接访问B页面的用户来说,无缓存情况下,页面加载的是[abcd]+[ef]两个资源包,既有冗余,又是两个请求,这并不是最理想的加载策略(这个方案是倾向于优化A页面展现的,虽然B页面首次展现不理想,但B页面大部分pv是从A页面导入,网站总体性能是更好的)。

而使用combo服务+LS的情况就不同了,假设combo的url的形式是[a,b,c,...],那么单独访问A、B页面的资源url就是:
  • A页面使用url:[a,b,c,d]
  • B页面使用url:[a,b,d,e,f]
用户由A页面进入网站,加载[a,b,c,d]这个url,然后LS缓存4个资源,再跳转到B页面,缓存控制框架可以知道本地缓存了哪些,然后只发起[e,f]这个请求。其效果基本等效于浏览器基于URL的缓存。而对于那些没有通过A页面直接访问B页面的用户来说,B页面加载的是[a,b,d,e,f],也是不错的合并策略。LS在这个时候就发挥了那么一点点优势。

当然,这种优势还不够明显,最能展现LS优势的,其实是单页面应用。因为单页面应用需要完全有JS管理页面状态,并增量加载资源,用户也可能通过带有hash的url直接访问某个单页面中的虚拟页面,同一个页面会有很多种不同的资源请求组合,这个时候,唯有LS+combo才能很好的解决资源加载与缓存问题。对于这种情况,我有一个网站可以用于展示效果:

Scrat - webapp模块化开发体系

这是部署在github上的页面,没有combo服务,但是可以一定程度上展现LS缓存的效果,我把js、css都缓存到LS中了,有兴趣的同学可以查看这个webapp中不同页面间的 首次访问二次访问页面间跳转 等过程中资源的加载效果。这个网页的源代码在这里:scrat-team/scrat-site · GitHub 通过travis-ci自动构建到这里的:scrat-team/scrat-team.github.io · GitHub

总结一下

PC上应用价值不大的原因在于:
  • 兼容性不太好,不支持LS的浏览器比例仍然很大
  • 网络速度快,协商缓存响应快,LS读取+eval很多时候会比不上304
  • 通常需要SEO,导致css不能缓存,仅缓存js使得整个缓存方案意义进一步减小
  • 浏览器本地缓存足够可靠持久
  • 跨页面间共享缓存即便有浪费也差别不大
移动端webapp值得一试的原因在于:
  • 兼容性好
  • 网速慢,LS读取+eval大多数情况下快于304
  • 都说是webapp了,不需要seo,css也可以缓存,再通过js加载
  • 浏览器缓存经常会被清理,LS被清理的几率低一些
  • 以模块文件为单位,缓存失效率低
  • 不同页面状态直接访问、二次访问、页面状态跳转资源组合是不确定的,不能通过url来缓存资源,否则就不“增量”啦
另外,LS缓存作为缓存,我们要先假设它是不可靠的,使用原则就是“LS缓存存在就用,没有就直接加载”,就是所谓的平稳退化(或渐进增强)。那些“写满了”,“写不了”,“被清了”等情况一概当做没有缓存。从统计的角度来看,在合适的模式下开启缓存全局总是有正收益的

顺便做个广告,以下UC的产品都是使用这套模块化开发体系进行开发的,后端均为nodejs,UC的前端开发者大多为全栈工程师:
还有一些UC only的产品就不贴了,不方便查看源代码和网络情况。。。

最后感慨一下:
前端性能优化既是一个工程问题,又是一个统计问题。

===============[ 补充 ]===============
收到一些反馈,这里做一下补充:
  • 这是一种“黑科技”,因为LS本身并不是被设计用来干这件事的。从过往历史来看,任何黑科技都是短暂且不可靠的,但就在当下,我也想不到什么更好的工程手段来提升移动端webapp的性能,所以,LS+combo的方案可以说是“有总比没有强”
  • 在未来,HTTP2时代的到来应该会完美绝杀这种黑科技,因此,工程化的具体实施方法必然要与时俱进,不过工程化的方法论不会过时,无论在哪个时代,我们都应该全面、科学的分析工程问题,结合当前的浏览器环境和技术手段来做方案,保持网站的性能
  • 有安全领域大神 @EtherDream 指出,“静态资源存 localStorage 就是水坑漏洞的前兆,风险远远大于优化”,这点我也认可,一旦有xss漏洞,就会被人利用将恶意代码注入到LS中,导致即便修复了xss恶意代码也存在的问题。所以我们现在采用的策略是每次部署新版本就会清除全部缓存。这会导致缓存利用率的下降,不过至少还有部分浏览器缓存在呢,算是一个折中处理。
  • 腾讯网前端团队做的 MT 方案利用LS把更新机制精细到了字符级别,这个确实更加“丧心病狂”,哈哈。不过看到他们运行的很不错,应该是更加优秀的缓存控制方案。
  • 可编程的缓存控制,我觉得这是一个值得深挖的方向,对前端的工程化价值非常大,尤其是与统计结合起来,根据网站的访问统计、页面间的步长关系计算出最合理的缓存和打包策略,这也是一种方向,【基于统计的资源打包+浏览器缓存】几乎可以完败【combo+LS】,不过这套系统的基础设施建设成本颇高
  • TBC
先说说主要会面临的问题
1、执行速度,读取后使用eval或创建<script>标签的时间会比浏览器直接加载慢。
2、版本控制,需要自己写一套版本控制机制。
3、localStorage是公共资源,如果你的产品域名下有很多应用共享这份资源会有风险。
4、localStorage以页面的域名划分,而常见的静态资源都以资源本身的域名来缓存,意味着如果你的应用有多个等价域名,它们之间的localStorage不互通,会造成缓存多份浪费。
5、兼容性需要处理(不支持、隐私模式、写满、http/https、写的正确性等等)

=====================================
下面开始胡扯时间!
如果上述2、3、4、5你觉得都不是问题的话,1一定程度上是可以量化来评估的。
方案1:使用原始的缓存机制
做静态资源缓存率的收集得到比例:A%
完整请求所需时间:t0
304所需时间:t1
在使用浏览器内建机制的情况下代码执行所需时间:t2
那么使用浏览器内建机制所需的时间期望就是:T1 = t0 * (1-A%) + t1 * A% + t2
* 如果使用expires方式的HTTP缓存可以让t1为0

方案2:使用自定义的localStorage缓存机制
做localStorage缓存命中率测试得到比例:B%
在使用localStorage读取并执行的情况下代码执行所需时间:t3
使用localStorage的缓存命中情况比较复杂,可以稍微梳理一下:
1、B%概率命中localStorage,所需时间为t3
2、(1-B%)概率未命中localStorage,A%概率命中HTTP缓存,所需时间为t1 + t2
3、(1-B%)未命中localStorage,(1-A%)概率未命中HTTP缓存,所需时间为t0 + t2
其中2和3加起来就是(1-B%) * T1对吧
那么使用localStorage缓存机制所需的时间期望就是:T2 = t3 * B% + T1 * (1-B%)

* 上面忽略了localStorage缓存机制的加载器自己加载的时间、它的逻辑内部消耗和写入localStorage等时间。

不严谨,不过我觉得还是有参考价值的
从上面的两个时间上看,我们预期是T2 < T1
delta = T2 - T1 = t3 * B% - T1 * B% = (t3 - T1) * B%
因为B%肯定>=0,要让delta<0,只要t3比T1小就行了(B%会决定收益有多大)
其实说了这么半天,也就是楼上@小爝 所言
读localstorage再eval的速度比直接加载304缓存在当成js的执行速度要慢,而且不少……呵呵。。。
因为T1当中其实还包含一定的完整加载的情况,可能结论会不至于这么悲观。

把T1展开分析一下,具体就不写这里了,结论是:
  1. 浏览器内建缓存命中率越高,对localStorage方案越不利
  2. 手工读取并执行代码vs内建机制执行代码的速度差越多,对localStorage方案越不利
实际例子:出于某种奇特的原因,HTTP缓存命中率很低,而用户的浏览器性能其实都还不错,这种情况下方案2会比较有可能性优于方案1。

当然如果有兴趣的话应该做实验来验证……
为什么?