在移动web开发上,如何优化性能一直是首要解决的问题,特别是在网络不稳定,网络带宽低的情况下,如何更好的提升用户体验,那更是至关重要。预加载,分段加载,缓存等则是常用的手段之一。当移动web开发遇上localStorage
的时候,前端开发都心花怒放,春天要来了。网络差不要紧,使用缓存,加载慢不要紧,还是使用缓存。使用缓存之后,用户体验的确是真的好了,产品开心了,前端开发就往缓存里写写写,当localStorage
成为万能药膏,突然间冬天就来了,日志中报错发现有的客户端localStorage
被写满了,导致功能无法正常使用,只能自己挖的坑自己填了。
在填坑之前,我们先考虑了使用缓存需要注意的问题:
- 缓存只是为了提升性能,不能认为缓存一定可用,数据获取失败或写入失败都应该有后续的代替处理
- 有可能会存在部分缓存数据写入之后,长期都未使用过或者代码中已不再会使用该缓存(已失去效用的数据占用了空间)
- 缓存的使用都是开发直观上的认为有需要,但是写入之后,后续是否真正有使用到(可能该功能用户只使用一次之后就不再使用),是否也是非必要占用了缓存的空间
- 数据缓存保存在客户端,需要有一定的机制来控制缓存的增长或者做缓存的清除
在最开始的阶段,采用的方案是强制规范使用localStorage
,主要的流程如下:
- 后端增加接口,返回允许设置的缓存
key
列表,此列表由后端维护,保证了所有客户端的缓存数据都是在此列表中(一般保持在100个以内),对于无用或已不再使用的缓存,清除该key
- 在客户端增加缓存初始化脚本,在加载完页面之后,读取缓存
key
列表,对于不在此列表中的缓存做删除处理 - 调整写入缓存的函数,如果写入的
key
未在允许缓存的key
列表中,不写入缓存,并发送统计日志至后端,根据相关统计日志,调整代码
在调整为此方式之后,客户端的缓存是能控制了,但是使用缓存就变得麻烦了,每次增加一个缓存都需要增加key
到列表中,不使用的时候还要做清除,经常性的是key
又到了上限之后,开会讨论哪些key
是无用或者用途不大的,清理一批。如此如此,大家都觉得这完全不是正路,完完全全就是每月一次,每次痛一痛。
大家都不想每月痛一痛,那么就思考新的解决方案啦。缓存数据有可能不清除导致一直增长,那么就调整一下,让它能自动清除,因此就把使用localStorage
的流程再次调整:
- 不再直接使用
localStorage
,重新封装一个缓存库,每个写入到localStorage
的数据都增加createdAt
ttl
字段 - 在客户端增加缓存初始化脚本,在加载完页面之后,初始化定时清理缓存任务:扫描整个
localStorage
,对过期的缓存做清理
在使用此方式之后,客户端的缓存也是能控制了,但是ttl
的设置就变得有点难选择了,设置短了很快就会变清除了,设置长了,又怕到时缓存被占满了,因为有些功能会连续的写缓存(例如小说阅读把后面的章节做缓存)。后面发现这种方式对于缓存的控制还是比较的简陋,ttl
的选择要比较靠经验,很好保证是否能利用好缓存。
下面再来考虑以下的场景:小说阅读的时候,在加载完当前章节时,为了用户体验,会自动的加载后面的章节,这些章节的缓存应该怎么设置ttl
,这是一个很麻烦的问题。用户有可能看着就停下来了,去忙其它的事,如果设置短了,那么缓存基本没什么用。那么设置长一点呢?下面这种场景又坑了。用户先打开第一章(此时加载完之后,立即预加载了后面5章,wifi环境,速度快),用户发现想看的不是这一章,又翻去了第十章,连续几次这样的操作,缓存里就多了些无用的数据了。
需要缓存能够控制,在空间足够的时候,能保存多久则多久,最终选择了使用lru-store结合阶段一使用的方式,流程如下:
- 后端定义缓存的
namespace
以及各namespace
缓存的的max-length
- 在客户端增加缓存初始化脚本,在加载完页面之后,对不在列表中的
namespace
做清除 lru-store
根据列表中返回的namespace
以及max-length
初始化,缓存的清除则是根据lru
自动清除
通过如此方式调整之后,对整体的localStorage
可控,基本只需要定义好namespace
与其max-length
就好,在使用过程中,一般配置的namespace
不超过10个,每个的长度基本在20以下,也不需要经常的做增加、删除。lru-store
则能保证每个缓存的数据量不会超过特定的阀值,而使用得多的缓存也能更好的利用。
在后续阶段,收集缓存的命中、清除等事件,根据统计数据调节各缓存的max-length
,以前那种滥用缓存的情况不复存在,使用缓存也不再需要考虑其它的因素了。
注: lru-store
每次更新都会将整个namespace
的数据写到localstorage