小程序性能优化三板斧

为何有这篇文章

想看干货的能够直接跳转到正文 ......css

小程序中心是百度 APP小程序流量分发的入口,从百度我的中心能够进入。html

image

小程序中心说大不大,说小也不小,属于麻雀虽小五脏俱全的那种,从 18 年到如今经历了 2 年的迭代,经手了 20 多任开发,1000 次左右的 commit ,也发展成了一个比较成熟的产品。产品发展到必定阶段,就开始呈现出技术上的一些瓶颈,前期为了快速的上线功能埋下了很多的坑,尤为是性能上的坑,达到了不可忽视的程度。前端

可是坑嘛,嘛,仍是须要后人一点点填上的,因此因此这个“稍显稍显“艰巨的任务天然而然的落在了接手这个小程序的个人身上,随后便开始了小程序中心的性能优化之路。git

第三季度对性能优化进行了排期,经历了一系列“神奇的操做”,小程序中心的 FMP 从 2100ms 下降到了如今的 1300ms。针对小程序性能优化也有了一些经验,总结了一套方法,在组内作了分享,口若悬河的讲了两个小时,可是也许讲的太方法论了些,组内的小伙伴看起来都听的一迷一迷的。甚至会后仍是会被问“怎么作才能快速的提高小程序的性能呢???”。github

其实性能提高永远没有捷径,须要分析、优化、实验、监控,须要一点点积累和深刻。随着你对项目和性能优化理解不断深刻,会发现提高性能的手段变得愈来愈丰富,性能数据天然也会跟着上去。但,你可能仍是要问“那么怎么作才能快速的提高小程序的性能呢”。web

image
好吧,不装了,我摊牌了,(敲黑板!)如下是一些简单有效的方法,并且几乎能够无脑应用到全部小程序中npm

什么?你说你不会?好吧,我把源代码也给你贴上去了,ctrl+c ctrl+v总会吧!该怎么作你看着办。小程序

性能优化的背景

在探讨性能优化以前,首先须要须要知道什么是性能。当咱们讨论到性能时,实际上是讨论应用在不一样的环境条件、输入、外界因素下是否能有一致的、稳定的、快速的响应。咱们不但愿用户由于程序代码写法上的问题而致使本身的需求受到影响。咱们但愿的是,应用能够快速的响应、流畅的切换,用户在知足本身需求的过程当中感受不到停顿和等待。在小程序中,性能能够收敛于三个指标,FMP白屏率服务可用性,下面讲一下这三个指标的意义。后端

FMP: First Meaningful Paint,即首次有意义的绘制。FMP 一般是最重要的指标,标志了程序在通常状况下的应用表现,FMP 高了说明程序首次加载时间较长,也就是用户须要等待较长的时间才能进入到小程序中,在这个过程当中用户可能就会选择退出了,FMP 低说明用户很快就能够进入到小程序中,给用户的感受就是快,减小了用户等待的时间。api

白屏率:用户触发页面打开后,间隔必定时间后仍然没有任何页面绘制,则认定为白屏,白屏率 = 白屏发生 PV / 小程序冷启动打开 PV。白屏率一般是极端状况下的应用表现,好比在无网、弱网、后端无返回或返回错误状况下的行为,虽然大部分状况下不能给用户有用的信息,可是须要有兜底的策略防止用户得不到反馈,若是得不到反馈用户就会认为是程序出了问题,他不会去考虑环境的问题,也不会去 debug ,你可能就会所以失去一个用户。

服务可用性:包括

  1. HTTP请求访问失败率:请求后端服务时的失败率,失败率 = 请求失败次数 / 请求数量。
  2. JSError:小程序运行过程当中发生的 JS error。

服务可用性表明了错误状况下的应用表现,错误按照来源方简单分为两种,一个是服务器端的错误,具体的表现就是HTTP请求失败,一种是前端的错误,也就是JS error。这些错误有可能什么都不影响,但也可能严重到致使程序异常不能运行,须要具体问题具体分析。

你能够在 开发者平台-开发管理-运维中心

image
看到这三个指标的详细状况。咱们能够看到白屏率和服务可用性其实标志了应用的稳定性和错误/异常场景下的表现,而 FMP ,是在正常的业务场景下最直观的描述小程序性能的指标,下面咱们就围绕如何“如何下降小程序 FMP 讲一下提高小程序性能的“三板斧”。

第一板斧-断舍离,减小小程序包体积

咱们知道,小程序在发布的时候都是先将本地的代码打个包,而后上传到服务器,用户在使用咱们的小程序时首先会先下载代码包,而后宿主app中的小程序框架【todo,小程序核心是什么意思??】会根据代码包进行渲染。用户的网络状况咱们不能控制,但代码包的大小咱们仍是能够把控的。减小代码包体积就是一种最简单也是最直接的方法【todo,可能会被argue,不少开发者作了体积裁剪,可是并不生效】。

能删除的资源删除,实在不能删除的压缩

用户打开小程序时只会看到一个页面,那么咱们能够把其它页面都删掉,只保留这一个页面,这样FMP就能够降下去。

image

手动狗头保命,固然不能这么作,除非饭碗不想要了...

可是这个思路是能够借鉴的。事实上,若是你的小程序经历过了屡次迭代,经手过了不一样的开发人员以后,你会发现,小程序的功能更完善了,包体积也不断的增长了,然而,这些页面这些功能真的都是必须的嘛?在 开发者平台-数据分析-行为分析-页面分析-页面访问量

image

能够看到你的小程序各个页面流量的状况,对大部分的小程序而言,流量只集中在少数的几个页面上,有些页面根本没有流量,那这些没有流量的页面与功能是否是也能够从小程序中摘除呢?固然能够。

从小见大,没有用的页面能够删除,没有用到的资源也能够从小程序包中删除,包括自定义组件、npm 包、css、图片。

在智能小程序开发的过程当中,常常须要引入图片资源。若是使用图片不当(过多过大的图片),在加载时会消耗更多的系统资源,从而影响整个页面的性能,所以作好图片优化很是重要。【todo,这个话术不必定合适,能够参看一下 https://smartprogram.baidu.co... 这篇文章里的说明 update:已改成“在智能小程序开发的过程当中,常常须要引入图片资源。若是使用图片不当(过多过大的图片),在加载时会消耗更多的系统资源,从而影响整个页面的性能,所以作好图片优化很是重要。“】,小程序包中的图片会随小程序包一块儿下载,而这些图片其实能够放到静态资源服务器上,小程序代码中直接使用图片地址就好。若是特别须要使用图片,别忘了在小程序开发者工具-项目信息-本地配置-上传代码时开启图片压缩。

将入口页占比较高的页面分到主包,其它页面分到子包

分包 是小程序官方提供的减小包体积的方法,开发者能够将智能小程序划分红不一样的子包,在构建时打包成不一样的分包,用户在使用时按需进行加载。建议按照 开发者平台-数据分析-行为分析-页面分析-入口页面次数

image
降序来分包,将作入口页多的页面放到主包中,其它的页面适当的分包便可。

须要注意的是,在分包以后,页面的路径也会变化,若是以前某些页面作过推广活动,为了防止用户找不到页面,可使用 自定义路由 的功能将原地址映射到新地址上。

第二板斧-存数据,巧用缓存与官方能力

快速的展现首屏是咱们的目的,为了快速的展现首屏,有些东西要放弃,有些东西要妥协。使用官方提供的性能优化的方法,虽然不是那么优雅,但确实是提高性能的好手段。而缓存这种用空间换取时间的策略,在性能优化的方法上是真的实用有效。

使用 prelink ,使用 onInit

prelink 只需在 开发者平台-开发管理-设置-开发设置-服务器配置

图片

配置,你就能够获得 200ms 的提高,这简直是官方给你的尚方宝剑,用不用看你了。它的原理是提早创建 TCP 链接和复用 TCP 链接。须要注意的是,配置的请求地址是须要支持 HEAD 类型请求的。

onInit 是官方给你的又一个魔法,只须要把 onLoad() 中的获取数据的方法在 onInit() 中再进行一遍便可。就这么简单。

// 修改前

onLoad() {

this.getPageData();

}

// 修改后

onInit() {

if (!this.onInitLoaded) {

this.onInitLoaded = true;

this.getPageData();

}

},

onLoad(options) {

if (!this.onInitLoaded) {

this.onInitLoaded = true;

this.getPageData();

}

}

缓存 API 端能力

API端能力是小程序提供的不一样于普通 web 应用的功能,这些功能方便了开发者去实现丰富的应用,但端能力其实是有性能消耗的,和普通的 js 语句相比执行起来要慢一些,为了抹平这种差别,一些不常变化的 API 端能力结果其实能够缓存起来,屡次获取时直接从咱们缓存的数据中获取

const cached = swan.getStorageSync('apiResultCached') || {};

const promiseCache = new Map();

const MAX_CACHE_TIME = 1000 * 60 * 60 * 24 * 7;

// 缓存方法

function memorize(fn) {

const apiName = fn.name;

return function () {

if (cached[apiName]) {

if (Date.now() - cached[apiName]['__timestamp'] < MAX_CACHE_TIME) {

return Promise.resolve(cached[apiName]);

}

cached[apiName] = null;

}

let promise = promiseCache.get(apiName);

if (promise) {

return promise;

}

promise = new Promise((resolve, reject) => {

fn().then(res => {

cached[apiName] = res;

cached[apiName]['__timestamp'] = Date.now();

swan.setStorage({

key: 'apiResultCached',

data: cached

});

resolve(res);

}).catch(e => {

reject(e);

}).finally(() => {

promiseCache.delete(apiName);

});

});

promiseCache.set(apiName, promise);

return promise;

};

}

function getSystemInfoAPI() {

return new Promise((resolve, reject) => {

swan.getSystemInfo({

success: res => resolve(res),

fail: err => reject(err)

});

});

}

// 这里只缓存了swan.getSystemInfo,一些其它的API方法,只要是不长变化的均可以缓存起来

export const getSystemInfo = memorize(getSystemInfoAPI);

缓存页面主数据

若是页面的数据是静态的,直接写到 Pagedata 中便可,但实际大部分状况是,页面一部分是前端就能够渲染的静态的结构与数据,另外一部分是从后端接口获取的数据。从后端接口获取的首屏数据能够缓存到 storage 中,这样在第二次加载这个页面的时候能够从 storage 中获取,同时异步发起请求,请求返回后再更新页面数据。注意,咱们是为了更快的展示页面,因此只缓存和加载首屏可见的数据便可,非首屏数据延迟加载

// 从storage中获取页面数据

swan.getStorage({

key: 'pageData',

success: res => {

// 若是有缓存且异步请求未返回则使用缓存的数据渲染页面

if (res.data && !this.requestBack) {

this.renderPage(data);

}

}

});

// 异步发起请求获取页面数据

getPageData().then(res => {

this.requestBack = true;

// 请求返回后根据最新数据渲染页面

this.renderPage(res.pageData);

// 同时缓存页面数据到storage中

swan.setStorage({

key: 'pageData',

data: res.pageData

});

});

这样作可能会带来一个问题,就是页面数据加载后并不必定是最新的数据,最新的数据从请求获取到后会刷新页面的数据。因此,若是你的应用对实时性的要求比较高的话可能并不适合使用这种方法。

第三板斧-轻渲染,只渲染必须的内容

在小程序加载过程当中,逻辑代码和渲染代码是分离的,分别由不一样的线程进行。

image
慢的线程会拖累整个加载的速度,当你的逻辑代码已经跑的飞起的时候,能够考虑下是否在渲染的层面有改进的办法。

减小对渲染有消耗的写法

小程序自己提供了丰富多彩的用法,包括自定义组件动态库filtersjs等等,这些功能提高了咱们开发的效率,但另外一方面,多种多样的功能有可能带来新的的性能消耗陷阱。你须要在效率和性能之间找寻一种平衡,有哪些用法提高的效率有限而带来的性能消耗倒是不可忽视的?这须要结合自身业务的实践,但在 FMP 占比较高的页面,这些功能仍是须要慎之又慎。

另外,也须要注意 减小view和text组件的特殊属性和事件 ,这是很容易忽视的一点,虽然单次使用带来的性能消耗有限,可是要用到 view 和 text 组件的地方太多了,架不住使用数量的上升带来质的改变。尤为是自定义组件中使用了低性能的写法,由于自定义组件可能会被用到屡次(例如列表项,甚至可能会被用上百次上千次),低性能的自定义组件会带来成倍的性能消耗。

// 修改前 view 使用了 style 属性

<view style="height: 20rpx;">热门榜单</view>

// 修改后 view 使用了 class ,在 css 文件中写样式

.title {

height: 20rpx;

}

<view class="title">热门榜单</view>

分屏渲染

设想一下,当咱们加载一个长度超过一个屏幕的列表时,其实用户不会看到列表的全部内容,只能看到列表的前几项,那么咱们固然能够只加载列表的前几项,当用户滑动的时候再加载剩余的内容。一样的,在渲染页面的时候,咱们也能够在第一次 setData 时进行数据的分割,只设置首屏可见的数据,延迟设置非首屏数据

// appList是从后端接口获取的页面数据 active是当前可见的tab索引

// firstLoadAppList为计算出的首屏幕数据

const firstLoadAppList = appList.map((item, index) => {

return index === active ? item.slice(0, 10) : [];

});

this.setData({

appList: firstLoadAppList

}, () => {

// 可将完整数据记录待以后加载

this.appList = appList;

});

取消骨架屏采用渐进式加载

骨架屏 是小程序提供的一种优化用户体验的机制,但其实任何渲染都有消耗,骨架屏也是。在骨架屏中写了复杂的结构甚至动画效果,反而不利于真正的有意义的页面快速的加载。固然,骨架屏确实可让用户更快的感知到页面正在加载,因此须要在这之间寻找一种平衡,是须要用户先看到一个正在加载的页面,仍是让用户更快的看到有意义的有内容的画面。推荐的一个方案是:

  • 使用官方提供的骨架屏,但简化骨架屏的框架,减小使用样式与动画效果
  • 在真正的页面渲染中,为各个部分设置背景色与高度,在 Pagedata 中设置默认值,在还未进行第一次 setData 的时候渲染出页面的框架。这样,当页面数据来了的时候,只是在特定的部分填充值便可。

后记

欢迎在 小程序开发者社区 中提问性能相关的问题,也欢迎在Github上 follow我,我会不按期更新一些前端相关的文章,若是想更深刻的和我讨论小程序性能相关的问题,能够给我发邮件。