百度智能小程序 提前请求主数据
在 onInit 请求首屏主数据
基础库 3.160.12 及以上版本开始支持Page.onInit。
大部分小程序,需要发起至少一次网络请求并调用setData,才能完成整个页面的最终渲染。如果能优化该环节,页面的渲染时间将会大大缩短。
回顾一下我们在性能优化的原理和手段中所介绍的小程序启动流程,可以发现无论是把主数据请求放在App.onLaunch里还是Page.onLoad里,都会存在一些难以解决问题:
- 如果在App.onLaunch中请求主数据,那么每个页面的请求逻辑都需要在放在App的生命周期中,这样不仅造成了逻辑的耦合,也将一定程度影响初始数据initData的发送,继而拖慢渲染层的初次渲染。
- 如果在Page.onLoad中请求主数据,那么必须要等到渲染层完成firstRender之后才能请求主数据,时机比较晚。
onInit 简介
小程序提供一种页面级别的生命周期Page.onInit。
该生命周期的执行时机介于App.onLaunch和Page.onLoad之间。具体的执行时机可参考下图,小程序是在setInitData之后立即执行Page.onInit()。
如果把主数据请求从 Page.onLoad 转移到 Page.onInit 中,将极大提升小程序的页面加载性能。
开发者可以在onInit中向服务器请求数据,并执行setData。图中展示了setData的两种时机,同时应注意:
- 如果开发者setData发出的时机早于渲染线程的 FCP ,那么在onLoad中将能获取到本次setData的视图信息。
- 如果setData晚于 FCP ,那么onLoad中将获取不到本次setData的视图信息。
由于App.onShow是依赖客户端通知逻辑线程的,所以Page.onInit不一定会在首次 App.onShow之后执行。可确定的前后顺序是:App.onLaunch-> Page.onInit ->Component.created->Component.attached->Page.onLoad
如何使用 onInit
尽量将页面的业务数据请求放在 Page.onInit 中。
function getData(param) {
return new Promise((resolve, reject) => {
swan.request({
url: 'xxx',
success: res => resolve(res)
});
});
}
Page(
// 使用一个标记位,确保只请求一次主数据
hasRequest: false
data: {
value: ''
},
onInit(param) {
if (!this.hasRequest) {
this.hasRequest = true;
getData(param).then(res => {
this.setData({
value: res.data
});
})
}
},
onLoad(param) {
if (!this.hasRequest) {
this.hasRequest = true;
getData(param).then(res => {
this.setData({
value: res.data
});
})
}
}
);
使用建议
- 不能进行任何依赖视图层的操作,包括且不限于:
selectComponent
、selectAllComponents
、swan.createSelectorQuery
、swan.createMapContext
、swan.createCameraContext、
swan.createCanvasContext
等; - 由于并非所有版本的基础库都支持此生命周期,开发者可以参考上述代码片段,增加兼容逻辑;
- 如果您的小程序在逻辑线程初始化阶段存在较大瓶颈,那么使用Page.onInit可能不会有明显效果。建议从减少动态库和插件的使用、减少App.onLaunch耗时等角度进行优化。
onInit 的收益
此处以百度知道、百度百科和宝宝知道小程序为例使用Page.onInit进行了优化,上屏时长均有明显提升。
以下是三个小程序把主数据请求从 Page.onLoad 迁移到 Page.onInit 后,获取到的收益:
小程序
收益(单位 ms)
百度知道
210
百度百科
100
宝宝知道
150
prelink 预连接
百度App 11.26及以上版本开始支持prelink
prelink 简介
小程序首屏的渲染,通常依赖某个网络请求返回的数据,我们将这个请求定义为主请求。大部分情况下,主请求的速度提升,渲染速度也会随之提升。所以,小程序框架提供了prelink机制,用于提前和业务服务器建立网络连接,使随后的主请求可以复用该连接,提升请求速度。
- 使用prelink前后对比
通过上面的对比可以看出,在配置了 prelink 时,框架会在加载业务代码 app.js 的同时发送一个预连接请求。在发送主请求时,可以直接复用该连接,达到减少主请求时长的目的。
如何使用 prelink
正确使用 prelink,需要做以下两个操作:
1.在开发者平台配置 prelink 信息
- 进入开发者平台,在【开发管理】的【设置】点击【开发设置】。
- 找到【服务器域名】,点击【修改】。
- 在弹出的窗口中,找到【 prelink 的合法地址】,输入 prelink 地址后点击【保存并提交】。
- 该接口的域名需要和主请求域名保持一致。
- 支持配置多个预连接接口,对不同域名发起预连接。
- 该接口需要支持HEAD请求,其他规则(如域名校验、UA、REFERER 等)和 request 保持一致。
- 该接口的响应速度和稳定性均会影响 prelink 的效果,为了尽可能的减少预连接请求的响应时长,建议配置静态接口,并保证接口的稳定性。
2.在小程序业务代码中,尽可能前置需要复用预创建连接的请求(一般为主请求)。
如何测试 prelink
在百度App 中,提供了 prelink 的测试工具,用于开发者验证配置的正确性,开发者可按下图中的流程进行测试验证。
数据前置获取的优势
通过上文的 onInit,我们可以在大部分场景下,让主数据请求的时机更早,但在一些场景下,主数据的请求可能需要更早,或是主数据请求依赖其他一些需要耗时获取的数据作为参数,比如在一些商业线索类页面,商家会希望用户进入页面时立即看到线索相关信息,而不需要再等待,这种时候,我们需要一些能够更早获取数据的机制。本节,我们将介绍如何进行数据前置获取的优化手段。
什么是数据前置获取
“数据前置获取”,就是把关键页面数据获取的时机提前至前一个页面,当用户进入这些关键页面后,不再从后端请求数据,而是直接渲染从上一页面中获取到的数据。
通常情况下,用户看到一个有效页面的流程是:点击页面入口 → 进入页面 → 请求页面的数据 → 渲染请求到的数据。如果频繁访问某个页面的话,页面的数据也需要反复请求,不仅网络开销大,而且用户每次进入页面,都需要等待网络请求数据时间,用户体验较差。
通过数据前置获取,可以把关键页面的数据放在入口页面获取,页面跳转至关键页面时,将数据以页面参数的形式传输给落地页。这时候,用户看到关键页面的流程就变成了:点击页面入口 → 进入关键页面 → 渲染传输过来的数据。
不使用数据前置获取的用户体验
用户需要等待网络请求,一般约 200ms--400ms 左右:
使用数据前置获取的用户体验
关键页面直接获取前一页面拿到的数据,然后渲染数据,可以节省 200ms--400ms 的网络时间:
数据前置获取的优化方案示例
如果跳转的新页面页数据量较小(字段总数不超过 15 个、字段层级不超过 3 层),可以把数据放在跳转参数中传递到下一页:
- 在调用 swan.navigateTo 跳转到落地页时,将落地页所需的数据拼接在跳转 url 参数中。
toNextPage(pageData) { swan.navigateTo({ url: '/pages/to/next/path?pageData=' + encodeURIComponent(pageData) }); }
- 新页面在 onload 时,从 option 参数中取数据,并解析、渲染。
onLoad(option) { let pageData = option.pageData; // 转入对应的数据处理函数中 if (pageData) { this.setPageData(pageData); } }
如果跳转的新页面据量较大(字段总数在 15 个以上、字段层级超过 3 层),数据就不合适放在跳转参数中传递,可以放进全局变量中:
- 在调用 swan.navigateTo 跳转到落地页前,将落地页所需的数据存在全局变量中
toNextPage(pageData) { // 这里使用到的pageData全局变量,需要在app.js中定义 getApp().globalData.pageData = pageData; swan.navigateTo({ url: '/pages/to/next/path' }); }
- 新页面在 onload 时,从全局变量里获取页面所需的数据
onLoad(option) { if (getApp().globalData.pageData) { let pageData = getApp().globalData.pageData; // 转入对应的数据处理函数中 this.setPageData(pageData); // 清空全局变量 getApp().globalData.pageData = null; } }
使用注意事项
“关键页数据前置获取”,本质上是把关键页的数据获取成本转移到了入口页,所以并非所有的页面都可以使用。如要使用该手段优化某些页面,则页面必须满足如下条件:
- 数据较简单、数据量少(字段总数不超过 15 个、字段层级不超过 3 层);
- 该页面访问的频次较高,存在退出该页面后重新进入的情况;
数据持久化
在智能小程序中,我们把:从服务端获取到的数据,通过swan.setStorage方法保存在本地,页面渲染时直接从本地获取所需数据的这一行为叫做“数据持久化”。
为什么要把页面数据持久化
在包含了:固定 banner、筛选、导航等这些低时效性数据的页面里,把这些低时效性的数据存在本地,页面加载时直接从本地读取、渲染数据,会使页面上的留白时间大大减少,极大的提升用户体验,对白屏率指标也将有宜。
注意:对于存放在本地的数据,并非永远不再更新。而是在请求当前页面其他数据时,拿到最新的要持久化的数据,并将新数据替换进本地,供下次加载使用。
<未使用数据缓存的用户体验
使用数据缓存的用户体验
<
数据持久化的优化方案示例
- 定义存、取本地数据的工具方法:
从本地获取数据的方法:
function getLocalData(key) { return new Promise((resolve, reject) => { let group = 'homeData'; // 数据存储的组,开发者可按照页面进行分组,防止不同页面的数据互相覆盖 swan.getStorage({ key: group + '_' + key, success: res => { // console.log('get', res); let localData = res.data; resolve(localData); }, fail: err => { console.error('读取失败', err); reject(err); } }); }); } module.exports.getLocalData = getLocalData;
向本地存储数据的方法:
function setLocalData (data) { return new Promise((resolve, reject) => { let group = 'homeData'; // 数据存储的组,开发者可按照页面进行分组,,防止不同页面的数据互相覆盖 let keys = Object.keys(data); keys.forEach(item => { swan.setStorage({ key: group + '_' + item, data: data[item], success: res => { resolve(res); }, fail: err => { console.error(err); reject(err); } }); }); }); } module.exports.setLocalData = setLocalData;
- 在页面onload时,调用getLocalData方法,从本地取数据
const utils = require('./lib'); // 引入存取本地数据的公共方法 Page({ onload: function() { // 从缓存中获取指定的数据 utils.getLocalData('diamondzone').then(diamondzone => { // 把获取到的数据,传入对应的数据处理函数中 if (diamondzone) { this.setDiamondzoneData(diamondzone); } }); } })
- 在页面 request 方法的 success 回调中,调用setLocalData方法把新数据替换到本地
const utils = require('./lib'); // 引入存取本地数据的公共方法 Page({ request(path, params) { swan.request({ url: path, // 接口地址 data: params, // 接口参数 success: res => { utils.setLocalData(res.diamondzone); // 将从后端获取到的最新数据存进本地 } }) } })
使用注意事项
- 本地存储的数据总量不宜过大,建议维持在 1M 以内;
- “数据持久化”仅限于对时效性没有要求的数据使用,对于时效要求高的数据,不应存储在本地;