如果被耗时任务拖累,可能是姿势不对

  • 2019 年 12 月 4 日
  • 筆記

本文作者:IMWeb helinjiang 原文出处:IMWeb社区 未经同意,禁止转载

如果被耗时任务拖累,可能是姿势不对

在业务中,有时候需要处理一些相对耗时的事情,而且还有一些其他的逻辑还可能会依赖这个耗时任务。诚然,太久的耗时会对用户体验不好。

本文就自己在业务中的一次实践,从其中一个小角度来分享看法,纯属个人观点,如有纰漏之处,还望指教。

背景

我们的项目是Hybrid混合应用,页面运行在手机QQ(后续简称手Q)中。在我们的业务中,我们有个新上线的业务,进入页面A之后,需要根据用户的地理位置(可以用缓存)去跳转到新业务页面B(灰度)或者继续渲染页面A。

最正经的实现方案

很容易想到的最正(diao)经(si)的一种实现方案,就是先获得当前用户的地理位置,拿到地理位置之后,还要去调用后台CGI接口,获得当前用户处于的城市,然后再根据这个城市,判断是否应该跳转到特定的页面。

伪代码实现

使用伪代码表示如下:

// 调用手Q接口获取当前用户的地理位置,data中包含了经度和纬度  getLocation(function(data){      // 调用CGI接口,获得当前的城市信息      getCity(data, function(res){          if(res.city=='深圳') {              // 跳转到页面B              jumpTo('pageB');          } else {              // 继续渲染页面A              init();          }      });  });

你真不是来捣乱的吧

理论是美好的,但是…

  1. 客户端的 getLocation 接口去获取用户当前地理位置,其实是非常耗时的操作。经过多次测试,获取一次的耗时在 2s ~ 5s 左右,即便可以使用其缓存,也需要花费 200ms 以上,而且还很不稳定。要知道当时测试的环境是 wifi 下,而且测试机是比较新的配置中等的小米4。如果真实用户拿着一个配置不高的手机,然后在网络状况不好的场景下,我猜测耗时会更高。
  2. 除了客户端的接口会耗时之外,还需要调用一次后台CGI,由于这个CGI接口已经在现网运行过一段时间,从检测的数据来说,wifi下请求一次耗时大概在 50ms ~ 150ms 左右,如果在非wifi场景下,这个时间肯定会更耗时。
  3. webview本身初始化时间、js/css/img文件、其他CGI请求等的耗时等累积也在几百毫秒以上。

综上,如果用户第一次没有缓存的情况下,或者网络状况不好,或者他用的手机属于比较亲(di)民(duan)的那种,那么,当用户看着加载的菊花图一直转啊转,耳边不禁想起“跟着我左手右手一个慢动作,右手左手慢动作重播”时,我想应该没有多少人是会有耐心等待的。

进一步分析

上文讨论的最常规的方案显然会有体验的问题,怎么办?

需要时效性吗

对我们造成困扰的缘故,在于我们常会有一种程序员的思维(情怀),认为流程就是这样啊,这样才能够实现功能,而且是最精确最实时的。你看每次你进来我们的页面我们就对你进行一次判断,避免上一分钟你在城市A,下一分钟到了城市B会出问题。

没错,追求精确和完美并不是坏事,但这会付出时间成本,在这里而言,我们真的那么需要时效性吗?80% 以上的用户很少会在几小时或一天内会离开一座城市;即便离开了,看得到我们的新页面了,也没任何关系,更何况我们是在灰度,多几个人看到关系不大,以后迟早大家都看得到。

缓存地理位置

既然没有时效性的要求,要处理的业务也不是敏感,那么就缓存呗。且不是说缓存客户端的 getLocation 结果,而是直接缓存最后计算得到的城市名。

if (!existCity) {      // 调用手Q接口获取当前用户的地理位置,data中包含了经度和纬度      getLocation(function(data) {          // 调用CGI接口,获得当前的城市信息          getCity(data, function(res) {              if (res.city == '深圳') {                  // 跳转到页面B                  jumpTo('pageB');              } else {                  // 继续渲染页面A                  init();              }          });      });  } else {      // 继续渲染页面A      init();  }

用户的第一次

即便我们可以缓存地理位置,那用户第一次进来没有缓存时,依然逃脱不了加载缓慢的命运。难道要告诉用户说“忍一忍,第一次都会痛苦的,下一次再来时你就会感到畅快了”?对用户的第一次不负责,用户可能就不会给你第二次了。

提前准备缓存

既然在用户要的时候无法满足他们的需求,那么,何不提前准备呢?比如在一个浪漫温馨你侬我侬的夜晚,气氛恰到好处,却发现缺少了“必要的东西”,这时候需要你大晚上下楼跑几条街的店里去贡献一点GDP,你会崩溃的。

回到我们说的场景,似乎也可以提前发一个版本,在这个版本中,增加一个小功能,就是在用户正常打开页面之后,再私下去获取到用户的位置,并放入到本地 localStorage 中缓存结果。等到真实版本发出之后,由于之前已经有缓存结果了,那么就省略了调用接口的过程,而换成了判断 localStorage 缓存了,这个性能立即就上来了。

先领劵,再享受优惠

提前准备缓存的办法理论上是行得通的,但麻烦啊,要新发版本,而且为了一小部分灰度的人,影响了大部分人的体验(新版本发了之后之前的离线包或js文件缓存就失效了,何况他们又享受不到新的业务,浪费),有点不值得。就比如“必要的东西”是准备了,但没机会使用,也就浪费了。

进一步分析,还可以有进一步的优化。简单而言,就是用户第一次进来时,全都展示原始的页面A后,后台开始获取城市信息,并打上缓存(发放优惠劵,但也不是所有人都发,比如乞丐之类的就忽略了);第二次之后再来时,有指定的缓存了就直接跳转到页面B了(凭借优惠券享受优惠)。实际上我们最终的方案就是这种。

最后的方案和总结

上面说到了我们最终的方案是“先领劵再享受优惠”的思路。其实这和离线包的机制是类似的,如果本地没有离线包,则返回线上的,同时手Q后台线程会去拉取离线包到本地;等第二次再来时,由于有了离线包,这时候就直接使用离线包的内容,提升了用户体验。

很罗嗦的记录了最近在某个业务中遇到的情况,我想表达的内容也比较简单,就是如果我们依赖了很耗时的业务时,可以换种思维,换种姿势,例如将同步处理修改为异步处理,或者思考其他的方式,来解决技术无法解决问题。很典型的还有我们常见的进度条或者菊花loading图,也是从另外的方面的努力,来“掩盖”耗时的问题,而耗时问题有时候是无法避免的。