localStorage,您坑了吗?

在移动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

Last updated