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

背景

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

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

如何实现?

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

列表页的代码:

js 复制代码
navigateToOrderDetail({ order_uuid });

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

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

js 复制代码
// const orderDetail = await getOrderDetail({ order_uuid })
const orderDetail = store.state.orderDetail;

上机测试,问题来了:

  • 我们知道大部分用户用户页面加载都小于 600ms,此时接口 (800ms) 还未请求完,store 里面还没有值。
  • 有些场景是从外部直接进入订单详情,此时去读 store 也没有值。

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

目标是什么?

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

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

  1. 首先,对现有代码改动不能太大,最好就是两个函数,preloadusePreload,将现有请求进行包裹。
  2. preload 正在请求时,usePreload 要等待结果。
  3. 如果没有经过 preload,那 usePreload 需要正常发起请求。
  4. usePreload 只有第一次会使用 preload 的缓存,后面请求需要正常发起 (不然页面就没法刷新啦)。

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

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

js 复制代码
// 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();
}
js 复制代码
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 的前端都不是好厨师。

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

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

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

  1. 有些页面数据只要参数不变,返回数据是相同的,能不能在 usePreload 的时候判断一下如果参数不变,就不移除缓存呢?这样不仅页面快了,请求量还少了。
  2. 如果实现了第 5 点,那想在参数不变的时候强制刷新该怎么做呢?
  3. 虽然参数没变,但是缓存的数据是无效的,因为接口请求失败了,该怎么处理呢?
  4. 如果传入函数返回的不是一个 Promise,而在使用的时候 .then 读取不就会报错吗?

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

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

需要修改的点有:

  1. 修改 preloadMap 的结构,能够将每个请求对应的参数保存起来。
  2. 返回一定要是个 Promise,所以我们需要用 Promise 对传入的 request 进行包裹。
js 复制代码
// utils/preload.js
const preloadMap = {};

export function preload(key, request, deps) {
  preloadMap[key] = {
    promise: Promise.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

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

bash 复制代码
npm i @huolala-tech/preload-js
  • 预请求:
js 复制代码
import { preload } from '@huolala-tech/preload-js';

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

// pageB
const data = await usePreload('pageBData', pageBRequest);
  • 缓存:
js 复制代码
import { usePreload } from '@huolala-tech/preload-js';

const params = {
  data_id: 123,
  user_id: 456,
};
const data = usePreload(
  'someData',
  () => requestData(params),
  Object.values(params),
);
  • 强制刷新
js 复制代码
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 自动清除缓存
js 复制代码
import { setConfig } from '@huolala-tech/preload-js';
// 默认 removeOnCatch 为 true
setConfig({ removeOnCatch: false }); // 此设置为全局生效
js 复制代码
import { usePreload, removeOnCatch } from '@huolala-tech/preload-js';
// 单独设置请求
const data = await usePreload('userInfo', () => getUserInfo({ userId }), [
  useId,
  removeOnCatch(false), // 和参数依赖一样传入
]);

新的目标

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

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

相关推荐
米奇妙妙wuu12 分钟前
react使用sse流实现chat大模型问答,补充css样式
前端·css·react.js
傻小胖16 分钟前
React 生命周期完整指南
前端·react.js
梦境之冢1 小时前
axios 常见的content-type、responseType有哪些?
前端·javascript·http
racerun1 小时前
vue VueResource & axios
前端·javascript·vue.js
罗狮粉 991 小时前
docker部署微信小程序自动构建发布和更新
docker·微信小程序·notepad++
m0_548514771 小时前
前端Pako.js 压缩解压库 与 Java 的 zlib 压缩与解压 的互通实现
java·前端·javascript
AndrewPerfect1 小时前
xss csrf怎么预防?
前端·xss·csrf
Calm5501 小时前
Vue3:uv-upload图片上传
前端·vue.js
浮游本尊1 小时前
Nginx配置:如何在一个域名下运行两个网站
前端·javascript
m0_748239831 小时前
前端bug调试
前端·bug