抖音开放平台Logo
开发者文档
“/”唤起搜索
控制台

数据预取 JS 方案

收藏
我的收藏
说明
基础库 3.22 版本开始支持此能力。

使用场景

数据预取能够在小程序冷启动时提前发起请求,并缓存请求内容,在真实请求时使用缓存数据,减少网络请求的时间。在一些发起网络请求较晚的场景,增加数据预取,可以提升页面完全展现的速度
以西瓜视频小程序为例,在接入数据预取后,线上 FMP 从 1173ms 优化到 818ms,整体优化 300ms,速度提升约 30%(对比数据采用 0.5 倍速的启动对比)。
执行时机
方案影响
预取只是一个提取执行请求的附加优化方案,预取逻辑和小程序启动逻辑是并行启动的,在正常接入数据预取并且能够命中缓存的情况下,不会对服务器造成额外负担。因此需要注意预取的请求和小程序实际的请求一致,否则会引入额外的请求负担。

方案对比

配置方案
本方案(js 方案)
原理
通过 JSON 配置,客户端解析配置处理
独立 js 环境,开发者通过 js 控制要执行的逻辑
能力
    JSON 配置限制,只支持几个固定数据源参数并行请求,复杂参数以及串行场景受限。
    支持能力少,只支持接口预取
    所有的逻辑均为开发者控制,灵活性高。
    支持能力多,支持图片请求等更多能力
接入成本
略高,整体链路对开发者黑盒,不便调试
低,便于调试
适用场景
参数简单,n 个独立请求的简单场景。
配置方案不满足的复杂场景

代码接入

添加入口文件

注意
prelaunch.js 不支持引用其他文件,其他文件也无法引用它。

原生小程序

根目录新建 prelaunch.js / prelaunch.ts文件。
说明
当使用prelaunch.ts时需要额外在app.json文件中增加如下内容
"prelaunch": { "entry": "prelaunch.ts" }

Taro

    1.简单场景,prelaunch.js 为单独文件场景,使用taro提供的copy配置将 prelaunch.js copy到小程序产物下。https://docs.taro.zone/docs/config-detail#copy
    2.复杂场景,需要为prelaunch.js进行二次编译等。可将二次编译产物的输出路径设置为小程序产物跟目录的prelaunch.js 或者参考简单场景进行二次copy。

uni-app

只支持 cli 模式项目,将 prelaunch.js copy 到小程序产物目录下可参考如下方案。
    1.使用 uni-app 提供的官方能力,参考自定义静态文件的实现 copy 文件 https://zh.uniapp.dcloud.io/collocation/vue-config.html / https://zh.uniapp.dcloud.io/collocation/vite-config.html
    2.自定义 npm script。如下为一个参考示例
项目根目录 srcipts copyPrelaunch.js src prelaunch.js copyPrelaunch.js 文件 const fs = require('fs'); // 具体路径自定义 const path = require('path'); fs.copyFileSync(path.join(__dirname, '../src/prelaunch.js'), path.join(__dirname, '../dist/build/mp-toutiao/prelaunch.js')); package.json文件 "scripts": { "genPrelaunch": "node ./scripts/copyPrelaunch.js", "build:mp-toutiao": "... && npm run genPrelaunch", }

在 prelaunch 中监听页面的启动

在 prelaunch.js 中使用 registerOnPrelaunch 方法监听指定页面的启动。
注意:registerOnPrelaunch 的回调在小程序启动生命周期只会触发一次,当页面由于跳转/重定向等方式二次打开则不会再次触发。
registerOnPrelaunch(path: string, callback: (params: { path: string, query:string}) => { }) registerOnPrelaunch('app', callback); // 任意页面首次启动都会执行 registerOnPrelaunch(path, callback); // 特定页面首次启动都会执行

添加预取逻辑

registerOnPrelaunch('xxx',callback: (params: {path: string, query: string})=>{ // path 启动页路径 // query 启动页参数 tt.request({ url:url, id: 'id_test', // 唯一id,必须与消费时相同,自定义一个字符串即可,用于标识是一个需要复用的请求, // id和url相同的两个请求会复用 xxx,其余request参数 })});

消费触发的预取缓存

tt.request({ id: 'id_test', // 必须与触发时相同, usePrefetchCache: true// 必须为true xxx,其余request参数 })

调试&预览

真机

    4.2.3以下版本需要关闭局域网快速预览,使用真机预览。
    目前只支持vconsole 查看调试信息,可打开vconsole查看【Prelaunch】即可。

开发者工具

开发者工具4.2.3 & 基础库3.22.0 及以上版本支持.
    Console 搜索 Prelaunch
    调试方法:开发者可搜索 tt-prelaunch.js 文件进行调试

验证预取结果

开发者可通过查看 request 方法的回调返回值获取请求结果参考 数据预取的返回值
注意
图片预取目前暂不支持结果查询。

预取环境中支持的 API

类型
对外API
说明(未特殊说明与官方文档一致)
网络
tt.request
请求预取
新增 id 参数作为请求唯一标识, url (不带query)+ id相同即视为同一个请求。
预取结果情况,在request方法的结果中查看如下返回值信息
// 是否使用预取缓存 isPrefetch: boolean // 【预取结果描述】 // 其中 -1 为默认值; // 1~8 为未匹配原因; // 9 为没有任何缓存; // 22,23 为已发起预取,但返回结果失败或返回结果为空; // 100 为命中缓存; // 101 为命中请求中缓存,并成功; // 102 为命中请求中缓存,请求失败,为网络错误。 prefetchDetail:number // 数据预取的结果的详细信息 prefetchInfo: string | object
tt.preloadImages
图片预取
用于提前缓存指定urls对应的图片资源
// 参数: params.urls:array[] // 示例: preloadImages({urls: ['https://xxx']})
存储
tt.getStorage
tt.getStorageSync
tt.setStorageSync
tt.setStorage
tt.removeStorage
tt.clearStorage
tt.getStorageInfo
tt.getStorageInfoSync
信息
tt.getNetworkType
tt.getConnectedWifi
基础库版本>= 3.19.0 会拉起官方隐私授权弹窗
tt.getSystemInfo
tt.getSystemInfoSync
tt.getExtConfig
tt.getExtConfigSync
tt.getLaunchOptionsSync
tt.getEnvInfoSync
基础库 >= 3.22.0时支持,需要通过caniuse判断
授权相关
基础库版本>= 3.19.0 会拉起官方隐私授权弹窗
login不会拉起app登录面板
tt.getSetting
其他
tt.canIUse
预加载环境里可用的api

预取逻辑 接入必看

    id 和 url 不带 query 部分相同时认为是同一个请求。id 参数决定了请求是否能匹配,usePrefetchCache 参数决定是否要使用缓存,作用与数据预取配置方案中的数据预取_小程序_抖音开放平台匹配规则相同
    非预取发起的两个请求 url 如果不带 query 部分和 id 参数都相同时,也认为是同一个请求,如果传了 usePrefetchCache,第二次请求会命中第一次的缓存。
    只传入id,usePrefetchCache不传入或者为false时,会更新缓存,缓存key为id+url不带query。
🌰 现在有 tt.request http://a/?b=1tt.request http://a/?b=2 两个请求(不区分是否是预取环境)。
    如果调用 tt.request 时传入的 id 参数相同,那么第二个请求不会发起网络请求,会直接使用 http://a/?b=1 的接口。
    如果想让 tt.request http://a/?b=2 重新发起请求,可以选择以下方法:
    建议不传 id 参数,或显式设置 usePrefetchCache 参数为 false
    应在 id 参数生成逻辑中包含 b 参数的值。
    缓存有效期为 5min,app 重启后销毁。
    只预取 LCP 前的关键请求,否则,性能可能下降
    需要保证在 prelaunch.js 未执行或执行失败时,逻辑能正常运行
    尽量减少 prelaunch.js 的体积,建议小于 10k。

FAQ

    1.预取请求参数是精准匹配还是模糊匹配,具体指的是什么?除了指定 id 值相等外,url 的请求参数(文档中的 xxx,其余参数同 request)是否也必须完全相同才能匹配到预取的数据?
    a.精准匹配,id + 去掉 query 后的 url 是匹配的 key。其他参数不影响匹配。
    2.预取命不中后, 系统会自动切换到正常请求吗?小程序还需要做额外操作吗?
    a.会正常请求,不需要额外操作。
    3.跟以前的文档相比, 预取执行的时机变了。 以前是启动前,现在是启动时? 这时候会不会由于时序问题,造成命不中。具体指的是:预取策略是不是应用的较晚,以至于页面加载的时候获取数据还没有加载好?(理想场景是:当用户看到挂载锚点时触发预取)
    a.数据预取配置方案预取时机也是启动时。
    b.预取未完成时也能复用。
    c.预取后发起时,预取会复用逻辑侧重新发起的请求,不会有额外请求。
    4.registerOnPrelaunch 的回调仅包含页面路由及 query 参数,缺少启动场景值,存在一定的功能局限性
    5.数据预取的缓存过期时间
    a.5min,如果想要不命中, 需要控制id的生成逻辑或者usePrefetchCache参数不传或者为false
    6.业务接口参数需要 md5 签名
使用三方框架开发的小程序, prelaunch.js 因为不支持原生打包,所以普通开发者使用 crypto-js 时可能遇到问题。这里暂时提供一段精简后的crypto-js代码(只包含md5 / HmacMD5,core)。
var t=t||function(t,n){var r;if("undefined"!=typeof window&&window.crypto&&(r=window.crypto),"undefined"!=typeof self&&self.crypto&&(r=self.crypto),"undefined"!=typeof globalThis&&globalThis.crypto&&(r=globalThis.crypto),!r&&"undefined"!=typeof window&&window.msCrypto&&(r=window.msCrypto),!r&&"undefined"!=typeof global&&global.crypto&&(r=global.crypto),!r&&"function"==typeof require)try{r=require("crypto")}catch(t){}var e=function(){if(r){if("function"==typeof r.getRandomValues)try{return r.getRandomValues(new Uint32Array(1))[0]}catch(t){}if("function"==typeof r.randomBytes)try{return r.randomBytes(4).readInt32LE()}catch(t){}}throw new Error("Native crypto module could not be used to get secure random number.")},i=Object.create||function(){function t(){}return function(n){var r;return t.prototype=n,r=new t,t.prototype=null,r}}(),o={},s=o.lib={},a=s.Base={extend:function(t){var n=i(this);return t&&n.mixIn(t),n.hasOwnProperty("init")&&this.init!==n.init||(n.init=function(){n.$super.init.apply(this,arguments)}),n.init.prototype=n,n.$super=this,n},create:function(){var t=this.extend();return t.init.apply(t,arguments),t},init:function(){},mixIn:function(t){for(var n in t)t.hasOwnProperty(n)&&(this[n]=t[n]);t.hasOwnProperty("toString")&&(this.toString=t.toString)},clone:function(){return this.init.prototype.extend(this)}},c=s.WordArray=a.extend({init:function(t,n){t=this.words=t||[],this.sigBytes=null!=n?n:4*t.length},toString:function(t){return(t||f).stringify(this)},concat:function(t){var n=this.words,r=t.words,e=this.sigBytes,i=t.sigBytes;if(this.clamp(),e%4)for(var o=0;o<i;o++){var s=r[o>>>2]>>>24-o%4*8&255;n[e+o>>>2]|=s<<24-(e+o)%4*8}else for(var a=0;a<i;a+=4)n[e+a>>>2]=r[a>>>2];return this.sigBytes+=i,this},clamp:function(){var n=this.words,r=this.sigBytes;n[r>>>2]&=4294967295<<32-r%4*8,n.length=t.ceil(r/4)},clone:function(){var t=a.clone.call(this);return t.words=this.words.slice(0),t},random:function(t){for(var n=[],r=0;r<t;r+=4)n.push(e());return new c.init(n,t)}}),u=o.enc={},f=u.Hex={stringify:function(t){for(var n=t.words,r=t.sigBytes,e=[],i=0;i<r;i++){var o=n[i>>>2]>>>24-i%4*8&255;e.push((o>>>4).toString(16)),e.push((15&o).toString(16))}return e.join("")},parse:function(t){for(var n=t.length,r=[],e=0;e<n;e+=2)r[e>>>3]|=parseInt(t.substr(e,2),16)<<24-e%8*4;return new c.init(r,n/2)}},h=u.Latin1={stringify:function(t){for(var n=t.words,r=t.sigBytes,e=[],i=0;i<r;i++){var o=n[i>>>2]>>>24-i%4*8&255;e.push(String.fromCharCode(o))}return e.join("")},parse:function(t){for(var n=t.length,r=[],e=0;e<n;e++)r[e>>>2]|=(255&t.charCodeAt(e))<<24-e%4*8;return new c.init(r,n)}},l=u.Utf8={stringify:function(t){try{return decodeURIComponent(escape(h.stringify(t)))}catch(t){throw new Error("Malformed UTF-8 data")}},parse:function(t){return h.parse(unescape(encodeURIComponent(t)))}},d=s.BufferedBlockAlgorithm=a.extend({reset:function(){this._data=new c.init,this._nDataBytes=0},_append:function(t){"string"==typeof t&&(t=l.parse(t)),this._data.concat(t),this._nDataBytes+=t.sigBytes},_process:function(n){var r,e=this._data,i=e.words,o=e.sigBytes,s=this.blockSize,a=o/(4*s),u=(a=n?t.ceil(a):t.max((0|a)-this._minBufferSize,0))*s,f=t.min(4*u,o);if(u){for(var h=0;h<u;h+=s)this._doProcessBlock(i,h);r=i.splice(0,u),e.sigBytes-=f}return new c.init(r,f)},clone:function(){var t=a.clone.call(this);return t._data=this._data.clone(),t},_minBufferSize:0}),p=(s.Hasher=d.extend({cfg:a.extend(),init:function(t){this.cfg=this.cfg.extend(t),this.reset()},reset:function(){d.reset.call(this),this._doReset()},update:function(t){return this._append(t),this._process(),this},finalize:function(t){return t&&this._append(t),this._doFinalize()},blockSize:16,_createHelper:function(t){return function(n,r){return new t.init(r).finalize(n)}},_createHmacHelper:function(t){return function(n,r){return new p.HMAC.init(t,r).finalize(n)}}}),o.algo={});return o}(Math);!function(n){var r=t,e=r.lib,i=e.WordArray,o=e.Hasher,s=r.algo,a=[];!function(){for(var t=0;t<64;t++)a[t]=4294967296*n.abs(n.sin(t+1))|0}();var c=s.MD5=o.extend({_doReset:function(){this._hash=new i.init([1732584193,4023233417,2562383102,271733878])},_doProcessBlock:function(t,n){for(var r=0;r<16;r++){var e=n+r,i=t[e];t[e]=16711935&(i<<8|i>>>24)|4278255360&(i<<24|i>>>8)}var o=this._hash.words,s=t[n+0],c=t[n+1],d=t[n+2],p=t[n+3],y=t[n+4],g=t[n+5],w=t[n+6],v=t[n+7],_=t[n+8],m=t[n+9],B=t[n+10],b=t[n+11],x=t[n+12],H=t[n+13],S=t[n+14],z=t[n+15],C=o[0],M=o[1],A=o[2],D=o[3];C=u(C,M,A,D,s,7,a[0]),D=u(D,C,M,A,c,12,a[1]),A=u(A,D,C,M,d,17,a[2]),M=u(M,A,D,C,p,22,a[3]),C=u(C,M,A,D,y,7,a[4]),D=u(D,C,M,A,g,12,a[5]),A=u(A,D,C,M,w,17,a[6]),M=u(M,A,D,C,v,22,a[7]),C=u(C,M,A,D,_,7,a[8]),D=u(D,C,M,A,m,12,a[9]),A=u(A,D,C,M,B,17,a[10]),M=u(M,A,D,C,b,22,a[11]),C=u(C,M,A,D,x,7,a[12]),D=u(D,C,M,A,H,12,a[13]),A=u(A,D,C,M,S,17,a[14]),C=f(C,M=u(M,A,D,C,z,22,a[15]),A,D,c,5,a[16]),D=f(D,C,M,A,w,9,a[17]),A=f(A,D,C,M,b,14,a[18]),M=f(M,A,D,C,s,20,a[19]),C=f(C,M,A,D,g,5,a[20]),D=f(D,C,M,A,B,9,a[21]),A=f(A,D,C,M,z,14,a[22]),M=f(M,A,D,C,y,20,a[23]),C=f(C,M,A,D,m,5,a[24]),D=f(D,C,M,A,S,9,a[25]),A=f(A,D,C,M,p,14,a[26]),M=f(M,A,D,C,_,20,a[27]),C=f(C,M,A,D,H,5,a[28]),D=f(D,C,M,A,d,9,a[29]),A=f(A,D,C,M,v,14,a[30]),C=h(C,M=f(M,A,D,C,x,20,a[31]),A,D,g,4,a[32]),D=h(D,C,M,A,_,11,a[33]),A=h(A,D,C,M,b,16,a[34]),M=h(M,A,D,C,S,23,a[35]),C=h(C,M,A,D,c,4,a[36]),D=h(D,C,M,A,y,11,a[37]),A=h(A,D,C,M,v,16,a[38]),M=h(M,A,D,C,B,23,a[39]),C=h(C,M,A,D,H,4,a[40]),D=h(D,C,M,A,s,11,a[41]),A=h(A,D,C,M,p,16,a[42]),M=h(M,A,D,C,w,23,a[43]),C=h(C,M,A,D,m,4,a[44]),D=h(D,C,M,A,x,11,a[45]),A=h(A,D,C,M,z,16,a[46]),C=l(C,M=h(M,A,D,C,d,23,a[47]),A,D,s,6,a[48]),D=l(D,C,M,A,v,10,a[49]),A=l(A,D,C,M,S,15,a[50]),M=l(M,A,D,C,g,21,a[51]),C=l(C,M,A,D,x,6,a[52]),D=l(D,C,M,A,p,10,a[53]),A=l(A,D,C,M,B,15,a[54]),M=l(M,A,D,C,c,21,a[55]),C=l(C,M,A,D,_,6,a[56]),D=l(D,C,M,A,z,10,a[57]),A=l(A,D,C,M,w,15,a[58]),M=l(M,A,D,C,H,21,a[59]),C=l(C,M,A,D,y,6,a[60]),D=l(D,C,M,A,b,10,a[61]),A=l(A,D,C,M,d,15,a[62]),M=l(M,A,D,C,m,21,a[63]),o[0]=o[0]+C|0,o[1]=o[1]+M|0,o[2]=o[2]+A|0,o[3]=o[3]+D|0},_doFinalize:function(){var t=this._data,r=t.words,e=8*this._nDataBytes,i=8*t.sigBytes;r[i>>>5]|=128<<24-i%32;var o=n.floor(e/4294967296),s=e;r[15+(i+64>>>9<<4)]=16711935&(o<<8|o>>>24)|4278255360&(o<<24|o>>>8),r[14+(i+64>>>9<<4)]=16711935&(s<<8|s>>>24)|4278255360&(s<<24|s>>>8),t.sigBytes=4*(r.length+1),this._process();for(var a=this._hash,c=a.words,u=0;u<4;u++){var f=c[u];c[u]=16711935&(f<<8|f>>>24)|4278255360&(f<<24|f>>>8)}return a},clone:function(){var t=o.clone.call(this);return t._hash=this._hash.clone(),t}});function u(t,n,r,e,i,o,s){var a=t+(n&r|~n&e)+i+s;return(a<<o|a>>>32-o)+n}function f(t,n,r,e,i,o,s){var a=t+(n&e|r&~e)+i+s;return(a<<o|a>>>32-o)+n}function h(t,n,r,e,i,o,s){var a=t+(n^r^e)+i+s;return(a<<o|a>>>32-o)+n}function l(t,n,r,e,i,o,s){var a=t+(r^(n|~e))+i+s;return(a<<o|a>>>32-o)+n}r.MD5=o._createHelper(c),r.HmacMD5=o._createHmacHelper(c)}(Math); var CryptoJS = t; /** * * @param {string} data * @returns {string} */ function md5(data) { return CryptoJS.enc.Hex.stringify(CryptoJS.MD5(data)); } /** * * @param {string} data * @param {string} key * @returns {string} */ function HmacMd5(data, key) { return CryptoJS.enc.Hex.stringify(CryptoJS.HmacMD5(data, key)); }

完整示例代码

// prelaunch.js registerOnPrelaunch('app', function() { tt.request({ xxx }) }) // 监听index页面,只有index页面冷启动才会执行 registerOnPrelaunch('pages/index/index', function () { console.log('request---'); tt.request({ url: "xxx", id: 'xxx', success: function success(res) { console.log('tt.request', 'success'); tt.preloadImages({ urls: res.xximgs }); }, fail: function fail(e) { console.log('request error', e); } }); } // pages/index/index.js 逻辑代码 tt.request({ url: "xxx", // 如果prelaunch.js发起了url和id相同的请求,此时调用会直接复用之前发起的请求。 id: 'xxx', usePrefetchCache: true, // 标识是否要消费预期的请求,默认为false success: function success(res) { console.log('tt.request', 'success'); }, fail: function fail(e) { console.log('request error', e); } });