小程序性能优化之使用 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 上讨论。

相关推荐
耶啵奶膘24 分钟前
uniapp-是否删除
linux·前端·uni-app
王哈哈^_^2 小时前
【数据集】【YOLO】【目标检测】交通事故识别数据集 8939 张,YOLO道路事故目标检测实战训练教程!
前端·人工智能·深度学习·yolo·目标检测·计算机视觉·pyqt
cs_dn_Jie2 小时前
钉钉 H5 微应用 手机端调试
前端·javascript·vue.js·vue·钉钉
开心工作室_kaic3 小时前
ssm068海鲜自助餐厅系统+vue(论文+源码)_kaic
前端·javascript·vue.js
有梦想的刺儿3 小时前
webWorker基本用法
前端·javascript·vue.js
cy玩具4 小时前
点击评论详情,跳到评论页面,携带对象参数写法:
前端
qq_390161774 小时前
防抖函数--应用场景及示例
前端·javascript
无尽的大道5 小时前
Java反射原理及其性能优化
jvm·性能优化
John.liu_Test5 小时前
js下载excel示例demo
前端·javascript·excel
Yaml45 小时前
智能化健身房管理:Spring Boot与Vue的创新解决方案
前端·spring boot·后端·mysql·vue·健身房管理