评论

常见小程序优化方案总结

本文主要从四个方面做了总结,分别为:首次启动性能优化、渲染性能优化、生命周期优化、图片静态资源预加载;

一、首次启动性能优化

1、首次打开一个小程序,用户一般会观察到如下图所示的三种状态

这张图中的三种状态对应的都是什么呢?小程序启动时,微信会为小程序展示一个固定的启动界面,界面内包含小程序的图标、名称和加载提示图标。

此时,微信会在背后完成几项工作:下载小程序代码包加载小程序代码包初始化小程序首页。下载到的小程序代码包不是小程序的源代码,而是编译、压缩、打包之后的代码包。

2、小程序加载的顺序

微信会在小程序启动前为小程序准备好通用的运行环境。这个运行环境包括几个供小程序使用的线程,并在其中完成小程序基础库的初始化,预先执行通用逻辑,尽可能做好小程序的启动准备。这样可以显著减少小程序的启动时间。

通过这张图可以对比发现,小程序首次启动的 第一张图是资源准备(代码包下载);第二张图是业务代码的注入以及落地页首次渲染;第三张图是落地页数据请求时的loading态(部分小程序存在)。

3、优化方案

控制包大小:上传代码时要先进行压缩、静态图片资源除小的icon外其余放到cdn、无用代码清除;
分包加载:根据业务场景,将用户访问率高的页面放在主包里,将访问率低的页面放入子包里,按需加载;
分包预加载:在进入小程序某个页面时,由框架自动预下载可能需要的分包,提升进入后续分包页面时的启动速度。对于独立分包,也可以预下载主包。分包预下载 官方文档链接
独立分包技术:区别于子包,和主包之间是无关的,在功能比较独立的子包里,使用户只需下载分包资源;独立分包 官方文档链接

二、渲染性能优化

1、数据渲染优化

双线程下的界面渲染,小程序的逻辑层和渲染层是分开的两个线程。在渲染层,宿主环境会把WXML转化成对应的JS对象,在逻辑层发生数据变更的时候,我们需要通过宿主环境提供的setData方法把数据从逻辑层传递到渲染层,再经过对比前后差异,把差异应用在原来的Dom树上,渲染出正确的UI界面。

页面初始化的时间大致由页面初始数据通信时间和初始渲染时间两部分构成。其中,数据通信的时间指数据从逻辑层开始组织数据到视图层完全接收完毕的时间,数据量小于64KB时总时长可以控制在30ms内。传输时间与数据量大体上呈现正相关关系,传输过大的数据将使这一时间显著增加。因而减少传输数据量是降低数据传输时间的有效方式。

在数据传输时,逻辑层会执行一次JSON.stringify来去除掉setData数据中不可传输的部分,之后将数据发送给视图层。同时,逻辑层还会将setData所设置的数据字段与data合并,使开发者可以用this.data读取到变更后的数据。因此,为了提升数据更新的性能,可以参考如下方法:

  • 1.不要过于频繁调用setData,应考虑将多次setData合并成一次setData调用;
  • 2.数据通信的性能与数据量正相关,因而如果有一些数据字段不在界面中展示且数据结构比较复杂或包含长字符串,则不应使用setData来设置这些数据;
  • 3.与界面渲染无关的数据最好不要设置在data中,可以考虑设置在page对象的其他字段下;
  • 4.勿在后台页面去setData;
  • 5.建议创建一个检测data大小的方法,如果超过64K可以打印报警日志提醒开发者;

2、长列表优化方案

无限下拉加载后会大数据量展现导致的性能问题,一个常见的方法在诸多C端都有使用,一句话说就是"只渲染所需的元素"。虚拟列表是按需显示思路的一种实现,即虚拟列表是一种根据滚动容器元素的可视区域来渲染长列表数据中某一个部分数据的技术。简而言之,虚拟列表指的就是「可视区域渲染」的列表。有三个概念需要了解一下:

滚动容器元素:一般情况下,滚动容器元素是 window 对象。然而,我们可以通过布局的方式,在某个页面中任意指定一个或者多个滚动容器元素。只要某个元素能在内部产生横向或者纵向的滚动,那这个元素就是滚动容器元素考虑每个列表项只是渲染一些纯文本。在本文中,只讨论元素的纵向滚动。
可滚动区域:滚动容器元素的内部内容区域。假设有 100 条数据,每个列表项的高度是 50,那么可滚动的区域的高度就是 100 * 50。可滚动区域当前的具体高度值一般可以通过(滚动容器)元素的 scrollHeight 属性获取。用户可以通过滚动来改变列表在可视区域的显示部分。
可视区域:滚动容器元素的视觉可见区域。如果容器元素是 window 对象,可视区域就是浏览器的视口大小(即视觉视口);如果容器元素是某个 div 元素,其高度是 300,右侧有纵向滚动条可以滚动,那么视觉可见的区域就是可视区域。

实现虚拟列表就是在处理用户滚动时,要改变列表在可视区域的渲染部分,其具体步骤如下:

计算当前可见区域起始数据的 startIndex
计算当前可见区域结束数据的 endIndex
计算当前可见区域的数据,并渲染到页面中
计算 startIndex 对应的数据在整个列表中的偏移位置 startOffset,并设置到列表上
计算 endIndex 对应的数据相对于可滚动区域最底部的偏移位置 endOffset,并设置到列表上

虚拟列表的实现原理可以参考这篇文章:浅说虚拟列表的实现原理

3、长列表局部渲染技巧

在一个列表中,有n条数据,采用上拉加载更多的方式。假如这个时候想对其中某一个数据进行点赞操作,还能及时看到点赞的效果,可以采用setData全局刷新,点赞完成之后,重新获取数据,再次进行全局重新渲染,这样做的优点是:方便,快捷!缺点是:用户体验极其不好,当用户刷量100多条数据后,重新渲染量大会出现空白期(没有渲染过来)。

优化步骤:

  • 1.将点赞的id传过去,知道点的是那一条数据, 将点赞的id传过去,知道点的是那一条数据
    <view wx:if="{{!item.status}}" class=“btn” data-id="{{index}}" bindtap=“couponTap”>立即领取</view>

  • 2.重新获取数据,查找相对应id的那条数据的下标(index是不会改变的)

  • 3.用setData进行局部刷新
    this.setData({
    list[index] : newList[index]
    })

  • 4、用户事件优化
    视图层将事件反馈给逻辑层时,同样需要一个通信过程,通信的方向是从视图层到逻辑层。因为这个通信过程是异步的,会产生一定的延迟,延迟时间同样与传输的数据量正相关,数据量小于64KB时在30ms内。降低延迟时间的方法主要有两个。
    1.去掉不必要的事件绑定(WXML中的bind和catch),从而减少通信的数据量和次数;
    2.事件绑定时需要传输target和currentTarget的dataset,因而不要在节点的data前缀属性中放置过大的数据。

三、生命周期优化

  • 1、异步请求,页面渲染需要的数据最好在onLoad时异步请求数据,不要在onReady时请求;非页面渲染需要的数据,尽量放在onReady生命周期去调用;

  • 2、定时器、事件监听、播放组件、音视频组件等,在页面转入后台(onHide)或者销毁(onUnload)时应该中止掉;

四、图片静态资源预加载

在日常小程序的开发中,有很多的大图片是放置于cdn上的,在需要进行展示的时候,如果没有预加载有可能出现图片展示的不及时,造成不好的体验,所以如下方式实现了图片预加载的功能,可以封装成组件的形式。

实现思路是将图片添加进页面中,设置不可见,然后加载图片,实现一个预加载的功能。

1、添加模版文件: img-loader.wxml

<template name=“img-loader”>

<image mode=“aspectFill” wx:for="{{ imgLoadList }}" wx:key="*this" src="{{ item }}" data-src="{{ item }}" bindload="_imgOnLoad" binderror="_imgOnLoadError" style=“width:0;height:0;opacity:0” />

</template>

2、添加js文件:img-loader.js

/**

  • 图片预加载组件
    */

class ImgLoader {
/**

  • 初始化方法,在页面的 onLoad 方法中调用,传入 Page 对象及图片加载完成的默认回调
    */
    constructor(pageContext, defaultCallback) {
    this.page = pageContext
    this.defaultCallback = defaultCallback || function () { }
    this.callbacks = {}
    this.imgInfo = {}
this.page.data.imgLoadList = [] //下载队列
this.page._imgOnLoad = this._imgOnLoad.bind(this)
this.page._imgOnLoadError = this._imgOnLoadError.bind(this)

}

/**

  • 加载图片
  • @param {String} src 图片地址
  • @param {Function} callback 加载完成后的回调(可选),第一个参数个错误信息,第二个为图片信息
    */
    load(src, callback) {
    if (!src) return;
let list = this.page.data.imgLoadList,
  imgInfo = this.imgInfo[src]

if (callback)
  this.callbacks[src] = callback

//已经加载成功过的,直接回调
if (imgInfo) {
  this._runCallback(null, {
    src: src,
    width: imgInfo.width,
    height: imgInfo.height
  })

  //新的未在下载队列中的
} else if (list.indexOf(src) == -1) {
  list.push(src)
  this.page.setData({ 'imgLoadList': list })
}

}

_imgOnLoad(ev) {
let src = ev.currentTarget.dataset.src,
width = ev.detail.width,
height = ev.detail.height

//记录已下载图片的尺寸信息
this.imgInfo[src] = { width, height }
this._removeFromLoadList(src)

this._runCallback(null, { src, width, height })

}

_imgOnLoadError(ev) {
let src = ev.currentTarget.dataset.src
this._removeFromLoadList(src)
this._runCallback(‘Loading failed’, { src })
}

//将图片从下载队列中移除
_removeFromLoadList(src) {
let list = this.page.data.imgLoadList
list.splice(list.indexOf(src), 1)
this.page.setData({ ‘imgLoadList’: list })
}

//执行回调
_runCallback(err, data) {
let callback = this.callbacks[data.src] || this.defaultCallback
callback(err, data)
delete this.callbacks[data.src]
}
}

module.exports = ImgLoader

3、在需要使用预加载功能的xxx.wxml页面中加入模版文件和使用代码:

<import src="…/…/templates/img-loader.wxml"/>

<template is=“img-loader” data="{{ imgLoadList }}"></template>

4、在需要使用预加载功能页面的xxx.js文件中引入文件和使用代码:

import ImgLoader from ‘…/…/templates/img-loader.js’;

let images = [
http://cdn.weimob.com/saas/activity/bargain/images/arms/shoulie.png’,
http://cdn.weimob.com/saas/activity/bargain/images/arms/shandian.png’,
http://cdn.weimob.com/saas/activity/bargain/images/arms/fengbao.png
]

//初始化图片预加载组件,并指定统一的加载完成回调
this.imgLoader = new ImgLoader(this, this.imageOnLoad.bind(this));

images.forEach(item => {

this.imgLoader.load(item)

})

备注:如有错误请帮忙指出;如有侵权,请联系我们删除,谢谢!

最后一次编辑于  09-03  (未经腾讯允许,不得转载)
点赞 3
收藏
评论

1 个评论

  • 铭锋科技
    铭锋科技
    09-03

    反手一个赞

    09-03
    赞同
    回复 2
    • 微盟
      微盟
      09-03
      给你一个么么哒~
      09-03
      1
      回复
    • 小肥羊
      小肥羊
      10-31
      发现基情。。
      10-31
      回复