阅读(4089) (7)

百度智能小程序 提前请求主数据

2020-08-10 16:28:29 更新

在 onInit 请求首屏主数据

基础库 3.160.12 及以上版本开始支持Page.onInit。

大部分小程序,需要发起至少一次网络请求并调用setData,才能完成整个页面的最终渲染。如果能优化该环节,页面的渲染时间将会大大缩短。

回顾一下我们在性能优化的原理和手段中所介绍的小程序启动流程,可以发现无论是把主数据请求放在App.onLaunch里还是Page.onLoad里,都会存在一些难以解决问题:

  1. 如果在App.onLaunch中请求主数据,那么每个页面的请求逻辑都需要在放在App的生命周期中,这样不仅造成了逻辑的耦合,也将一定程度影响初始数据initData的发送,继而拖慢渲染层的初次渲染。
  2. 如果在Page.onLoad中请求主数据,那么必须要等到渲染层完成firstRender之后才能请求主数据,时机比较晚。

onInit 简介

小程序提供一种页面级别的生命周期Page.onInit。

该生命周期的执行时机介于App.onLaunch和Page.onLoad之间。具体的执行时机可参考下图,小程序是在setInitData之后立即执行Page.onInit()。

如果把主数据请求从 Page.onLoad 转移到 Page.onInit 中,将极大提升小程序的页面加载性能。

开发者可以在onInit中向服务器请求数据,并执行setData。图中展示了setData的两种时机,同时应注意:

  1. 如果开发者setData发出的时机早于渲染线程的 FCP ,那么在onLoad中将能获取到本次setData的视图信息。
  2. 如果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
                });
            })
		}
	}
);

使用建议

  1. 不能进行任何依赖视图层的操作,包括且不限于:selectComponentselectAllComponentsswan.createSelectorQueryswan.createMapContextswan.createCameraContext、swan.createCanvasContext等;
  2. 由于并非所有版本的基础库都支持此生命周期,开发者可以参考上述代码片段,增加兼容逻辑;
  3. 如果您的小程序在逻辑线程初始化阶段存在较大瓶颈,那么使用Page.onInit可能不会有明显效果。建议从减少动态库和插件的使用、减少App.onLaunch耗时等角度进行优化。

onInit 的收益

此处以百度知道、百度百科和宝宝知道小程序为例使用Page.onInit进行了优化,上屏时长均有明显提升。

以下是三个小程序把主数据请求从 Page.onLoad 迁移到 Page.onInit 后,获取到的收益:

小程序 收益(单位 ms)
百度知道 210
百度百科 100
宝宝知道 150



prelink 预连接

百度App 11.26及以上版本开始支持prelink

prelink 简介

小程序首屏的渲染,通常依赖某个网络请求返回的数据,我们将这个请求定义为主请求。大部分情况下,主请求的速度提升,渲染速度也会随之提升。所以,小程序框架提供了prelink机制,用于提前和业务服务器建立网络连接,使随后的主请求可以复用该连接,提升请求速度。

  • 使用prelink前后对比 配置prelink前后对比

通过上面的对比可以看出,在配置了 prelink 时,框架会在加载业务代码 app.js 的同时发送一个预连接请求。在发送主请求时,可以直接复用该连接,达到减少主请求时长的目的。


如何使用 prelink

正确使用 prelink,需要做以下两个操作:

1.在开发者平台配置 prelink 信息

  • 进入开发者平台,在【开发管理】的【设置】点击【开发设置】。 配置1
  • 找到【服务器域名】,点击【修改】。 配置2
  • 在弹出的窗口中,找到【 prelink 的合法地址】,输入 prelink 地址后点击【保存并提交】。 配置3
  • 该接口的域名需要和主请求域名保持一致。
  • 支持配置多个预连接接口,对不同域名发起预连接。
  • 该接口需要支持HEAD请求,其他规则(如域名校验、UA、REFERER 等)和 request 保持一致。
  • 该接口的响应速度和稳定性均会影响 prelink 的效果,为了尽可能的减少预连接请求的响应时长,建议配置静态接口,并保证接口的稳定性。

2.在小程序业务代码中,尽可能前置需要复用预创建连接的请求(一般为主请求)。


如何测试 prelink

在百度App 中,提供了 prelink 的测试工具,用于开发者验证配置的正确性,开发者可按下图中的流程进行测试验证。

preklink测试方法



数据前置获取的优势

通过上文的 onInit,我们可以在大部分场景下,让主数据请求的时机更早,但在一些场景下,主数据的请求可能需要更早,或是主数据请求依赖其他一些需要耗时获取的数据作为参数,比如在一些商业线索类页面,商家会希望用户进入页面时立即看到线索相关信息,而不需要再等待,这种时候,我们需要一些能够更早获取数据的机制。本节,我们将介绍如何进行数据前置获取的优化手段。

什么是数据前置获取

“数据前置获取”,就是把关键页面数据获取的时机提前至前一个页面,当用户进入这些关键页面后,不再从后端请求数据,而是直接渲染从上一页面中获取到的数据。

通常情况下,用户看到一个有效页面的流程是:点击页面入口 → 进入页面 → 请求页面的数据 → 渲染请求到的数据。如果频繁访问某个页面的话,页面的数据也需要反复请求,不仅网络开销大,而且用户每次进入页面,都需要等待网络请求数据时间,用户体验较差。

通过数据前置获取,可以把关键页面的数据放在入口页面获取,页面跳转至关键页面时,将数据以页面参数的形式传输给落地页。这时候,用户看到关键页面的流程就变成了:点击页面入口 → 进入关键页面 → 渲染传输过来的数据。

不使用数据前置获取的用户体验

用户需要等待网络请求,一般约 200ms--400ms 左右:

使用数据前置获取的用户体验

关键页面直接获取前一页面拿到的数据,然后渲染数据,可以节省 200ms--400ms 的网络时间:

数据前置获取的优化方案示例

如果跳转的新页面页数据量较小(字段总数不超过 15 个、字段层级不超过 3 层),可以把数据放在跳转参数中传递到下一页:

  1. 在调用 swan.navigateTo 跳转到落地页时,将落地页所需的数据拼接在跳转 url 参数中。
    toNextPage(pageData) {
       swan.navigateTo({
             url: '/pages/to/next/path?pageData=' + encodeURIComponent(pageData)
       });
    }
    
  2. 新页面在 onload 时,从 option 参数中取数据,并解析、渲染。
    onLoad(option) {
       let pageData = option.pageData;
       // 转入对应的数据处理函数中
       if (pageData) {
          this.setPageData(pageData);
       }
    }
    

如果跳转的新页面据量较大(字段总数在 15 个以上、字段层级超过 3 层),数据就不合适放在跳转参数中传递,可以放进全局变量中:

  1. 在调用 swan.navigateTo 跳转到落地页前,将落地页所需的数据存在全局变量中
    toNextPage(pageData) {
       // 这里使用到的pageData全局变量,需要在app.js中定义
       getApp().globalData.pageData = pageData;
       swan.navigateTo({
             url: '/pages/to/next/path'
       });
    }
    
  2. 新页面在 onload 时,从全局变量里获取页面所需的数据
    onLoad(option) {
       if (getApp().globalData.pageData) {
          let pageData = getApp().globalData.pageData;
          // 转入对应的数据处理函数中
          this.setPageData(pageData);
          // 清空全局变量
          getApp().globalData.pageData = null;
       }
    }
    

使用注意事项

“关键页数据前置获取”,本质上是把关键页的数据获取成本转移到了入口页,所以并非所有的页面都可以使用。如要使用该手段优化某些页面,则页面必须满足如下条件:

  1. 数据较简单、数据量少(字段总数不超过 15 个、字段层级不超过 3 层);
  2. 该页面访问的频次较高,存在退出该页面后重新进入的情况;


数据持久化

在智能小程序中,我们把:从服务端获取到的数据,通过swan.setStorage方法保存在本地,页面渲染时直接从本地获取所需数据的这一行为叫做“数据持久化”。

为什么要把页面数据持久化

在包含了:固定 banner、筛选、导航等这些低时效性数据的页面里,把这些低时效性的数据存在本地,页面加载时直接从本地读取、渲染数据,会使页面上的留白时间大大减少,极大的提升用户体验,对白屏率指标也将有宜。

注意:对于存放在本地的数据,并非永远不再更新。而是在请求当前页面其他数据时,拿到最新的要持久化的数据,并将新数据替换进本地,供下次加载使用。

<

未使用数据缓存的用户体验

使用数据缓存的用户体验

<

数据持久化的优化方案示例

  1. 定义存、取本地数据的工具方法:

    从本地获取数据的方法:

    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;
    
  2. 在页面onload时,调用getLocalData方法,从本地取数据
    const utils = require('./lib'); // 引入存取本地数据的公共方法
    Page({
        onload: function() {
            // 从缓存中获取指定的数据
            utils.getLocalData('diamondzone').then(diamondzone => {
                // 把获取到的数据,传入对应的数据处理函数中
                if (diamondzone) {
                    this.setDiamondzoneData(diamondzone);
                }
            });
        }
    })
    
  3. 在页面 request 方法的 success 回调中,调用setLocalData方法把新数据替换到本地
    const utils = require('./lib');  // 引入存取本地数据的公共方法
    Page({
        request(path, params) {
            swan.request({
                url: path,  // 接口地址
                data: params,   // 接口参数
                success: res => {
                    utils.setLocalData(res.diamondzone);    // 将从后端获取到的最新数据存进本地
                }
            })
        }
    })
    

使用注意事项

  1. 本地存储的数据总量不宜过大,建议维持在 1M 以内;
  2. “数据持久化”仅限于对时效性没有要求的数据使用,对于时效要求高的数据,不应存储在本地;