小程序性能优化之使用 preload-js 进行预加载

背景

在货拉拉小程序中有这样一个场景:从订单列表点击进入订单详情,由于订单详情是一个分包,所以可能会出现两次 loading,第一次是小程序分包加载,第二次是进入页面后接口请求。

而订单详情页面的静态资源加载时间主要分布在 200ms ~ 600ms 之间,接口的平均耗时在 800ms 左右。在订单列表点击的时候已经知道了要查看的订单,何不在点击的时候预请求订单详情接口,节省一个出接口加载的时间呢?

如何实现?

首先想到的,把数据放到全局状态里面。

列表页的代码:

navigateToOrderDetail({ order_uuid });

// 在跳转的同时请求详情数据
const orderDetail = await getOrderDetail({ order_uuid });
store.commit('setOrderDetail', orderDetail);

在订单详情页的代将原本的请求换成从 store 里面取数据。

// const orderDetail = await getOrderDetail({ order_uuid })
const orderDetail = store.state.orderDetail;

上机测试,问题来了:

  • • 我们知道大部分用户用户页面加载都小于 600ms,此时接口 (800ms) 还未请求完,store 里面还没有值。

  • • 有些场景是从外部直接进入订单详情,此时去读 store 也没有值。

而且此方案不够易用,如果要改造其他页面,那岂不是每个页面数据都要在 store 放一份?

目标是什么?

测试过后发现问题所在:在写代码之前都没有考虑下各种场景,这怎么能写出想要的代码呢?

赶紧先罗列一下我们的目标:

  1. 1. 首先,对现有代码改动不能太大,最好就是两个函数,preload 和 usePreload,将现有请求进行包裹。

  2. 2. 在 preload 正在请求时,usePreload 要等待结果。

  3. 3. 如果没有经过 preload,那 usePreload 需要正常发起请求。

  4. 4. usePreload 只有第一次会使用 preload 的缓存,后面请求需要正常发起 (不然页面就没法刷新啦)。

有了目标,我们开始写代码。

由于是跨页面使用,我们需要一个 key 将 preload 和 usePreload 关联起来。

// utils/preload.js
const preloadMap = {};

export function preload(key, request) {
  preloadMap[key] = request();
}

export function usePreload(key, request) {
  const cache = preloadMap[key];
  if (cache) {
    preloadMap[key] = null;
    return cache;
  }
  return request();
}
import { preload, usePreload } from '@/utils/preload';

navigateToOrderDetail({ order_uuid });
preload('orderDetail', () => getOrderDetail({ order_uuid }));

const orderDetail = await usePreload('orderDetail', () =>
  getOrderDetail({ order_uuid }),
);

等一下,代码就这么简单?是的,原因在于所有 api 请求都已经使用 Promise 进行封装。

从目标 2 (在 preload 正在请求时,usePreload 要等待结果) 可以猜出,要实现这个能力,非 Promise 莫属。

Promise 大家都很熟悉了,俗话说,没手写过 Promise 的前端都不是好厨师。

我们这里用到它的一个特性:preload 和 usePreload 返回同一个 Promise,如果页面先加载完而接口尚未返回,此时 Promise 在 pending 中,页面还是会正常展示请求 loading,但是请求时间减少为 800 - 页面加载时间;如果接口先返回,页面加载完后 Promise 已经是 fulfilled 状态,可以直接读取结果进行展示,此时接口请求时间为 0。

上机测试,完美实现 4 个小目标,收工!

不过在上线使用后,我们遇到了一些问题,然后又有了下面这几个新的目标:

5.有些页面数据只要参数不变,返回数据是相同的,能不能在 usePreload 的时候判断一下如果参数不变,就不移除缓存呢?这样不仅页面快了,请求量还少了。

6.如果实现了第 5 点,那想在参数不变的时候强制刷新该怎么做呢?

7.虽然参数没变,但是缓存的数据是无效的,因为接口请求失败了,该怎么处理呢?

8.如果传入函数返回的不是一个 Promise,而在使用的时候 .then 读取不就会报错吗?

要实现缓存,我们需要知道请求参数,只能增加第三个参数传入了:

// 将参数依赖传入 usePreload,判断依赖相同则返回缓存,否则发起新的请求
usePreload('driverInfo', () => getDriverInfo({ driverId, userId }), [
  driverId,
  userId,
]);

需要修改的点有:

  1. 1. 修改 preloadMap 的结构,能够将每个请求对应的参数保存起来。

  2. 2. 返回一定要是个 Promise,所以我们需要用 Promise 对传入的 request 进行包裹。

// utils/preload.js
const preloadMap = {};

export function preload(key, request, deps) {
  preloadMap[key] = {
    promisePromise.resolve(request()),
    deps,
  };
}

export function usePreload(key, request, deps) {
  const cache = preloadMap[key];
  if (cache) {
    const isDepsSame = deps.every((item, index) =>
      Object.is(item, cache.deps[index]),
    );
    if (isDepsSame) {
      return cache.promise;
    }
    preloadMap[key] = null;
    return cache;
  }
  return request();
}

用 Promise.resolve(request()) 是可以将返回变成 Promise,但这样不就永远都返回 fulfilled 状态了吗?

嘿嘿,小伙子,看来 Promise 基本功还不够扎实啊~(我不会说我又补过了才这么写的)

简单来说,不管嵌套多少层 Promise,只要中间没有 catch 拦截,在外面都是可以被 catch 到的。

另外,还有 6 和 7 两个目标还没实现呢,但是没关系,上面只是展示一下它的诞生过程,现在你只需要安装它然后使用。

@huolala-tech/preload-js

它的功能如上面所介绍,代码也开源了:https://github.com/HuolalaTech/preload-js。

npm i @huolala-tech/preload-js
  • • 预请求:

import { preload } from '@huolala-tech/preload-js';

// pageA
preload('pageBData', pageBRequest);
navigateTo('pageB');

// pageB
const data = await usePreload('pageBData', pageBRequest);
  • • 缓存:

import { usePreload } from '@huolala-tech/preload-js';

const params = {
  data_id123,
  user_id456,
};
const data = usePreload(
  'someData',
  () => requestData(params),
  Object.values(params),
);
  • • 强制刷新

import { usePreload, useDep } from '@huolala-tech/preload-js';

const [dep, refreshDep] = useDep();

const userId = store.state.userId;
const userInfo = await usePreload('userInfo', () => getUserInfo({ userId }), [
  userId,
  dep, // 同依赖参数一样传入
]);

function updateUserInfo() {
  refreshDep();
}
  • • Promise.catch 自动清除缓存

import { setConfig } from '@huolala-tech/preload-js';
// 默认 removeOnCatch 为 true
setConfig({ removeOnCatchfalse }); // 此设置为全局生效
import { usePreload, removeOnCatch } from '@huolala-tech/preload-js';
// 单独设置请求
const data = await usePreload('userInfo', () => getUserInfo({ userId }), [
  useId,
  removeOnCatch(false), // 和参数依赖一样传入
]);

新的目标

能否实现持久化本地缓存?即使在断网情况下打开,页面依然能渲染上一次的数据?

本文对 preload-js 的实现原理做了简单的介绍,完整的代码点击 HuolalaTech/preload-js[1] 查看。如果你有使用上的问题,欢迎到 Github 上讨论。

引用链接

[1] HuolalaTech/preload-js: https://github.com/HuolalaTech/preload-js


本篇文章来源于微信公众号:货拉拉技术

本文来自投稿,不代表TakinTalks稳定性技术交流平台立场,如若转载,请联系原作者。

(0)
上一篇 2023年12月4日 下午6:30
下一篇 2023年12月21日 下午3:59

相关推荐

发表评论

邮箱地址不会被公开。