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

相关推荐
这是个栗子2 分钟前
npm报错 : 无法加载文件 npm.ps1,因为在此系统上禁止运行脚本
前端·npm·node.js
毕设源码-邱学长28 分钟前
【开题答辩全过程】以 基于微信小程序的农商新闻网为例,包含答辩的问题和答案
微信小程序·小程序
小光学长28 分钟前
基于微信小程序的家具商城系统g80l9675(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
数据库·微信小程序·小程序
云起SAAS29 分钟前
1V1七彩测评抖音快手微信小程序看广告流量主开源
微信小程序·小程序·ai编程·看广告变现轻·1v1七彩测评
HIT_Weston33 分钟前
44、【Ubuntu】【Gitlab】拉出内网 Web 服务:http.server 分析(一)
前端·ubuntu·gitlab
sheji341633 分钟前
【开题答辩全过程】以 基于微信小程序的签到系统的设计与实现为例,包含答辩的问题和答案
微信小程序·小程序
华仔啊1 小时前
Vue3 如何实现图片懒加载?其实一个 Intersection Observer 就搞定了
前端·vue.js
JamesGosling6661 小时前
深入理解内容安全策略(CSP):原理、作用与实践指南
前端·浏览器
不要想太多1 小时前
前端进阶系列之《浏览器渲染原理》
前端
g***96902 小时前
Node.js npm 安装过程中 EBUSY 错误的分析与解决方案
前端·npm·node.js