数据预取
基础库 1.46.0 开始支持本功能
在小程序冷启动时提前发起请求,并缓存请求内容,在真实请求时读取缓存,减少网络请求的时间。
例如可以在文章详情页,商品详情页等高频访问的页面,增加数据预取的配置,可以提升页面完全展现的速度。
可以通过对比启动 FMP 耗时,衡量优化效果。以西瓜视频小程序为例,线上 FMP 从 1173ms 优化到 818ms,整体优化 300+ms,速度提升了 30%,效果还是很明显的。(为了更明显的效果,下图为 0.5 倍速的启动对比)

功能介绍
通常来说,小程序的请求都是在运行时调用 tt.request 的 API,并在 API 的回调中拿到返回。在运行时,小程序开发者可以对用户可能触发的下一步流程去进行数据的提前获取。但是如果首屏的数据依赖网络请求,小程序内能做的就不多了,所以小程序数据预取就应运而生啦。
小程序数据预取的主要功能就是在启动之前,提前按照小程序配置好的规则发起网络请求并缓存,这样在小程序启动后就可以直接使用缓存数据,来避免因为等待网络数据返回而展示的 loadingview 或页面展示不全的情况。简单的流程如下:

配置 app.json 文件
1.46.0 版本(deprecated)
会在未来的版本下线,app.json 新增 prefetches 配置项,用于设置预请求的所有 url 的列表,该部分 url,会在进入小程序后自动发起请求(先于开发者代码加载)。当开发者再次发起 request 请求时可以增加 usePreCache 参数,如果配置的 prefetch 请求已返回,则会直接返回请求结果,如果配置的 prefetch 请求还未返回,则当次 request 会继续之前未发送完成的 request 请求。
// app.json
{
prefetches:{
//页面路径
"pages/index/index": [
//请求url
"https://developer1.bytedance.com",
"https://developer2.bytedance.com",
"https://developer3.bytedance.com",
...
]
...
}
}
目前限定最多配置 10 个页面,每个页面最多 5 个预取请求,开发者服务器接口返回数据应为字符串类型,且大小不超过 256 K。
2.18.0 版本
基本逻辑保持一致,配置方式有所改变。app.json
新增 prefetchRules 配置项,prefetchRules 优先级更高,存在此配置项时旧的 prefetches 的配置不做处理。先来看一个示例:
// app.json
{
"prefetchRules": {
"pages/index/index": {//需要配置预取请求的页面
"https://developer.bytedance.com/${search}?a=${a}&b=2&c=3": {//请求的url模型
"method": "POST",//请求的方法
"header": {//请求的header
"a": "${a}",
"token": 1
},
"data": {//POST请求的body数据
"a": "${a}",
"token": 1
},
"responseType": "",//返回结果的类型
"hitPrefetchExtraRules": { // 模糊匹配规则,只要包含以下key,并且key对应的值相等就算命中
"requiredQueryKeys": ["a", "b", "c"], // 不考虑顺序
"requiredHeaderKeys": ["a", "token"]
}
},
...
}
}
}
我们把整个结构大致分成三层,第一层就是 prefetchRules
;第二层是页面路径,以及该路径下的页面配置;第三层就是单个请求的 url 以及请求的详细配置。
第一层,第二层比较简单,容易理解。第三层,也是最复杂的地方,url 与它的配置。他们会不仅会构成完整的请求,同时也具有模糊匹配的能力。下面我们先来说如何构成完整的请求————变量的填充
变量的填充(构造完整请求)
为了使配置的请求可以动态化的更改,我们引入了配置变量的概念。变量的格式就是 ${XXX}
,XXX 是变量名,变量赋值的过程就是字符串替换,用变量的值替换 prefetchRules
配置字符串中的 ${XXX}
。当所有的变量被数据源赋值后,就是一个合格的预取请求了。
基础版
构成请求最基础的就是 url,只要有完整 url 我们就可以发起请求了。目前我们支持 path,query 部分的变量替换,host 目前不支持。但是上述变量的值从哪里来呢,这就要说到数据预取的数据源了。目前数据源支持 schema 中 pagePath 的 query
和 storage
变量。举例说明:
// pagePath=pages/index/index?search=searchApp 小程序打开时的协议带有这个search参数,search 的值为searchApp
let aValue = tt.getStorageSync("a");//开发者的storage中存储了a变量,假设aValue的值为1
//赋值前
{
"prefetchRules": {
"pages/index/index": {
//-------- focus start----------------
"https://developer.bytedance.com/${search}?a=${a}&b=2&c=3": {...},
//-------- focus end------------------
...
},
...
}
}
//赋值后
{
"prefetchRules": {
"pages/index/index": {
//-------- focus start----------------
"https://developer.bytedance.com/searchApp?a=1&b=2&c=3": {...},
//-------- focus end------------------
...
},
...
}
}
生效顺序: schema 中 pagePath 的 query
,storage
(优先级逐渐变低,变量名相同时,优先使用 query 中的值)
优点 | 缺点 | |
---|---|---|
query变量 | 统一 | 无法针对单机,无法使用基础变量 |
storage变量 | 可以使用持久化的数据,可以使用运行时的数据 | 首次打开,变量不存在,无法使用 |
此时已经完成了基础配置,可以对数据进行预取了。但是如果开发者需要对请求有其他的配置,例如:method,header 等配置,就需要使用到进阶版本了。
进阶版
我们展开上述配置中的{...}部分,它包含 method,header,data 和 responseType,基本对应着 tt.request API 的各项参数。同样的,我们会在数据源中获取这部分配置中变量的值,并赋值
let aValue = tt.getStorageSync("a");//开发者的 storage 种存储了a变量,假设 aValue 的值为1
//赋值前
{
"prefetchRules": {
"pages/index/index": {
"https://developer.bytedance.com?a=${a}&b=2&c=3": {
//-------- focus start----------------
"method": "POST",
"header": {
"a": "${a}",
"token": 1
},
"data": {
"a": "${a}",
"token": 1
},
"responseType": "",
//-------- focus end-------------------
...
},
...
},
...
}
}
//赋值后
{
"prefetchRules": {
"pages/index/index": {
"https://developer.bytedance.com?a=" + aValue + "&b=2&c=3": {
//-------- focus start----------------
"method": "POST",
"header": {
"a": aValue,
"token": 1
},
"data": {
"a": aValue,
"token": 1
},
"responseType": "text",
//-------- focus end----------------
...
},
...
},
...
}
}
这部分参数都有对应的默认值,具体规则见下表:
参数名 | 数据类型 | 是否必传 | 默认值 | 说明 |
---|---|---|---|---|
header | object | optional | {'content-type': 'application/json'} | 请求 header |
method | string | optional | GET | 请求方法,支持 GET ,POST |
data | string/object | optional | null | 请求数据,命中需完全一致(不支持GET请求传,会改为空字符串) |
responseType | string | optional | text | 响应数据类型,可选值:text |
经过以上的配置与数据的填充,我们可以构造出合格的数据预取请求了,但是因为上述填充规则描述的配置项基于一个请求来说并不算少,所以如果需要完全匹配才算命中的话,有些严苛了,以至于会拉低缓存的命中率。所以我们支持了模糊匹配。
缓存的复用(精确匹配与模糊匹配)
在判断缓存是否能复用时,有精确匹配与模糊匹配两种策略。匹配时默认为精确匹配,精确匹配会比较每一个配置,包括 host,path,query,method,header 等。如果请求中的冗余参数比较多,我们建议使用模糊匹配规则。
下面我们来看示例配置中的最后一块,模糊匹配规则 hitPrefetchExtraRules,可以进一步提升缓存的命中率。
// app.json
{
"prefetchRules": {
"pages/index/index": {
"https://developer.bytedance.com/${search}?a=${a}&b=2&c=3": {
...
//-------- focus start----------------
"hitPrefetchExtraRules": {
"requiredQueryKeys": ["a", "b", "c"],
"requiredHeaderKeys": ["a", "token"]
}
//-------- focus end------------------
},
...
},
...
}
}
requiredQueryKeys
、requiredHeaderKeys
规则描述:
- 该字段的值为一个字符串数组;
- 该字段不存在时,命中规则为严格匹配,只有在
tt.request
请求的配置和预取请求的配置,完全相同时才会命中; - 当该字段存在时,在对比配置的
query
或header
时,只会比较在requiredKeys
数组中的 key 和 key 对应的 value 是否相等,其他值不做比较。
举例说明:
// 预取请求
"https://developer.bytedance.com/search?a=1&b=2&c=3"
// 命中规则
hitPrefetchExtraRules:{ "requiredQueryKeys": ["a", "b", "c"] }
// tt.request 请求
// 命中
"https://developer.bytedance.com/search?a=1&b=2&c=3&d=4"
// 不命中,c的值不相等
"https://developer.bytedance.com/search?a=1&b=2&c=4"
// 不命中,缺少c
"https://developer.bytedance.com/search?a=1&b=2&d=4"
// 预取请求
"https://developer.bytedance.com/search?a=1&b=2&c=3":{
header:{a:1,b:2,c:3}
}
// 命中规则
{ "requiredHeaderKeys": ["a", "c"] }
// tt.request 请求
//不命中,query严格匹配,多了d
"https://developer.bytedance.com/search?a=1&b=2&c=3&d=4"
// 命中,b是不影响判断的key
"https://developer.bytedance.com/search?a=1&b=2&c=3":{
header:{a:1,b:5,c:3}
}
// 不命中,a的值不一致,且缺少c
"https://developer.bytedance.com/search?a=1&b=2&c=3":{
header:{a:3,b:2}
}
经过上述的配置,已经可以进行完整的数据预取了,下面我们最后来说一下,如何使用预取的缓存。
代码示例
tt.request 接口新增 usePrefetchCache
参数,返回数据新增 isPrefetch
区分数据是否为预取。举例说明:
//开发者配置app.json
{
"prefetchRules":{
"pages/index/index": {
"https://developer1.bytedance.com?testid=${id}&testdata=${sData}":{},
"https://developer1.bytedance.com/${sid}?testid=${id}&testdata=${sData}":{
"method": "POST",
"header": {
"testCookie": "${sCookie}",
"token": "xxxs1823730"
},
"data": {
"mData": "${mData}"
},
"responseType": "",
"hitPrefetchExtraRules": {
"requiredQueryKeys": ["testid", "testdata"],
"requiredHeaderKeys": ["testCookie", "token"]
}
}
...
}
...
}
}
//storage
const testCookie = tt.getStorageSync("sData");
const sCookie = tt.getStorageSync("sCookie");
const mData = tt.getStorageSync("mData");
//request
const token = "xxxs1823730";
const { sid, testid } = option.query;
const url = `https://developer1.bytedance.com/${sid}?testid=${testid}&testdata=${sData}`;
const header = { testCookie, token };
const data = { mData };
tt.request({
url,
header,
data,
method: "POST",
dataType: "json",
responseType: "text",
usePrefetchCache: true,
success: (res) => {
console.log("返回数据是否来自预取:", res.isPrefetch);
console.log("请求数据:", res.data);
},
});
Bug & Tip
- Tip:只会触发当次启动首页(start_page)的数据预取配置,如果 start_page 为空,会触发默认入口页的数据预取配置。预取请求的缓存可以被用于非首页请求,缓存在当次启动生效。
- Tip:
prefetchRules
配置中的变量在 url 中只有 query 和 path 部分有效,host 部分不支持配置,第一个变量与 host 之间至少要有一个/
或?
间隔。例如:developer.bytedance.com${id}
形式的 url 不支持预取。 - Tip:预请求 url 中参数处理逻辑与
tt.request
接口保持一致。
// app.json
{
prefetchRules:{
//页面路径
"pages/index/index": {
//请求url
"https://developer.bytedance.com?a=${a}&b=${b}&c=${c}":{
"method": "POST",
"header": {
"x": "${d}",
"token": 1
},
"data": {
"y": "${f}",
"token": 1
},
"responseType": "",
"hitPrefetchExtraRules": {
"requiredQueryKeys": ["c"],
"requiredHeaderKeys": ["x", "token"]
}
},
...
}
...
}
}
信息流落地页参数处理过程如下:
// 信息流落地页配置
let a = "中国";
let b = encodeURIComponent("中国");
let c = encodeURIComponent(encodeURIComponent("中国"));
let pagePath = `pages/index/index?a=${a}&b=${b}&c=${c}`;
// pages/index/index?a=中国&b=%E4%B8%AD%E5%9B%BD&c=%25E4%25B8%25AD%25E5%259B%25BD
// pages/index/index.js
page({
onLoad(option) {
console.log(option.query.a); // 中国
console.log(option.query.b); // 中国
console.log(option.query.c); // encodeURIComponent('中国')
let d = tt.getStorageSync("d");
let f = tt.getStorageSync("f");
let url =
"https://developer.bytedance.com?a=${option.query.a}&b=${option.query.b}&c=${option.query.c}";
// 'https://developer.bytedance.com?a=中国&b=中国&c=%E4%B8%AD%E5%9B%BD'
let header = { x: d, token: 1 };
let data = { y: f, token: 1 };
let method = "POST";
let responseType = "text";
tt.request({
url: url,
header: header,
data: data,
method: method,
dataType: "json",
responseType: "text",
usePrefetchCache: true,
success(res) {
console.log(`request调用成功 ${res}`);
},
fail(res) {
console.log(`request调用失败`);
},
});
// tt.request 会对url中参数做urlencode,已encode的参数不会重复urlencode
// tt.request 请求开发者服务器的url
// 'https://developer.bytedance.com?a=%E5%8C%97%E4%BA%AC&b=%E5%8C%97%E4%BA%AC&c=%E4%B8%AD%E5%9B%BD'
// 预请求url参数处理逻辑与tt.request保持一致
// 'https://developer.bytedance.com?a=%E5%8C%97%E4%BA%AC&b=%E5%8C%97%E4%BA%AC&c=%E4%B8%AD%E5%9B%BD'
},
});
分享参数传递如下:
// pages/index/index.js
onShareAppMessage() {
let a = '中国';
let b = encodeURIComponent('中国');
let c = encodeURIComponent(encodeURIComponent('中国'));
let pagePath = `pages/index/index?a=${a}&b=${b}&c=${c}`;
// pages/index/index?a=中国&b=%E4%B8%AD%E5%9B%BD&c=%25E4%25B8%25AD%25E5%259B%25BD
return {
title: 'title',
desc: 'desc',
path: pagePath,
imageUrl: 'https://e.com/e.png',
success() {
},
fail() {
},
};
}
// 通过分享落地页打开小程序
// pages/index/index.js
page({
onLoad(option) {
console.log(option.query.a); // 中国
console.log(option.query.b); // encodeURIComponent('中国')
console.log(option.query.c); // encodeURIComponent(encodeURIComponent('中国'))
let url =
"https://developer.bytedance.com?a=${option.query.a}&b=${option.query.b}&c=${option.query.c}";
// 'https://developer.bytedance.com?a=中国&b=%E4%B8%AD%E5%9B%BD&c=%25E5%258C%2597%25E4%25BA%25AC'
let header = { x: d, token: 1 };
let data = { y: f, token: 1 };
let method = "POST";
let responseType = "text";
tt.request({
url: url,
header: header,
data: data,
method: method,
dataType: "json",
responseType: "text",
usePrefetchCache: true,
success(res) {
console.log(`request调用成功 ${res}`);
},
fail(res) {
console.log(`request调用失败`);
},
});
// tt.request 会对url中参数做urlencode,已encode的参数不会重复urlencode
// tt.request 请求开发者服务器的url
// 'https://developer.bytedance.com?a=%E5%8C%97%E4%BA%AC&b=%E5%8C%97%E4%BA%AC&c=%25E5%258C%2597%25E4%25BA%25AC'
// 预请求url参数处理逻辑与tt.request保持一致
// 'https://developer.bytedance.com?a=%E5%8C%97%E4%BA%AC&b=%E5%8C%97%E4%BA%AC&c=%25E5%258C%2597%25E4%25BA%25AC'
},
});