mpvue性能优化实战技巧

2次阅读

共计 7824 个字符,预计需要花费 20 分钟才能阅读完成。

最近一直在折腾 mpvue 写的微信小程序的性能优化,分享下实战的过程。
先上个优化前后的图:可以看到打包后的代码量从 813KB 减少到 387KB,Audits 体验评分从 B 到 A,效果还是比较明显的。其实这个指标说明不了什么,而且轻易就可以做到,更重要的是优化小程序运行过程中的卡顿感,请耐心往下看。
常规优化
常规的 Web 端优化方法在小程序中也是适用的,而且不可忽视。
一、压缩图片
这一步最简单,但是容易被忽视。在 tiny 上在线压缩,然后下载替换即可。
我这项目的压缩率高达 72%,可以说打包后的代码从 813KB 降到 387KB 大部分都是归功于压缩图片了。
二、移除无用的库
我之前在项目中使用了 Vant Weapp,在 static 目录下引入了整个库,但实际上我只使用了 button,field,dialog 等几个组件,实在是没必要。
所以干脆移除掉了,微信小程序自身提供的 button,wx.showModal 等一些组件基本可以满足需求,自己手写一下样式也不用花什么时间。
在这里建议大家,在微信小程序中,尽量避免使用过多的依赖库。
不要贪图方便而引入一些比较大的库,小程序不同于 Web,限制比较多,能自己写一下就尽量自己写一下吧。
小程序的优化
咱们首先得看一下官方优化建议,大多是围绕这个建议去做。
一、开启 Vue.config._mpTrace = true
这个是 mpvue 性能优化的一个黑科技啊,可能大多数同学都不知道这个,我在官方文档都没有搜到到这个配置,我真的是服了。我能找到这个配置也是 Google 机缘巧合下看到的,出处:mpvue 重要更新,页面更新机制进行全面升级具体做法是在 /src/main.js 添加 Vue.config._mpTrace = true,如:

Vue.config._mpTrace = true
Vue.config.productionTip = false
App.mpType = ‘app’
添加了 Vue.config._mpTrace 属性,这样就可以看到 console 里会打印每 500ms 更新的数据量。如图:如果数据更新量很大,会明显感觉小程序运行卡顿,反之就流畅。因此我们可以根据这个指标,逐步找出性能瓶颈并解决掉。
二、精简 data
1. 过滤 api 返回的冗余数据
后端的 api 可能是需要同时为 iOS,Android,H5 等提供服务的,往往会有些冗余的数据小程序是用不到的。比如 api 返回的一个文章列表数据有很多字段:
this.articleList = [
{
articleId: 1,
desc: ‘xxxxxx’,
author: ‘fengxianqi’,
time: ‘xxx’,
comments: [
{
userId: 2,
conent: ‘xxx’
}
]
},
{
articleId: 2
// …
},
// …
]
假设我们在小程序中只需要用到列表中的部分字段,如果不对数据做处理,将整个 articleList 都 setData 进去,是不明智的。
小程序官方文档:单次设置的数据不能超过 1024kB,请尽量避免一次设置过多的数据。
可以看出,内存是很宝贵的,当 articleList 数据量非常大超过 1M 时,某些机型就会爆掉(我在 iOS 中遇到过很多次)。
因此,需要将接口返回的数据剔除掉不需要的,再 setData,回到我们上面的 articleList 例子,假设我们只需要用 articleId 和 author 这两个字段,可以这样:
import {getArticleList} from ‘@/api/article’
export default {
data () {
return {
articleList: []
}
}
methods: {
getList () {
getArticleList().then(res => {
let rawList = res.list
this.articleList = this.simplifyArticleList(rawList)
})
},
simplifyArticleList (list) {
return list.map(item => {
return {
articleId: item.articleId,
author: item.author
// 需要哪些字段就加上哪些字段
}
})
}
}
}
这里我们将返回的数据通过 simplifyArticleList 来精简数据,此时过滤后的 articleList 中的数据类似:
[
{articleId: 1, author: ‘fengxianqi’},
{articleId: 2, author: ‘others’}
// …
]
当然,如果你的需求中是所有数据都要用到(或者大部分数据),就没必要做一层精简了,收益不大。毕竟精简数据的函数中具体的字段,是会增加维护成本的。
PS:在我个人的实际操作中,做数据过滤虽然增加了维护的成本,但一般收益都很大,因次这个方法比较推荐。
2. data() 中只放需要的数据
import xx from ‘xx.js’
export default {
data () {
return {
xx,
otherXX: ‘2’
}
}
}
有些同学可能会习惯将 import 的东西都先放进 data 中,再在 methods 中使用,在小程序中可能是个不好的习惯。
因为通过 Vue.config._mpTrace = true 在更新某个数据时,我对比放进 data 和不放进 data 中的两种情况会有差别。
所以我猜测可能是 data 是会一起更新的,比如只是想更新 otherXX 时,会同时将 xx 也一起合起来 setData 了。
3. 静态图片放进 static
这个问题和上面的问题其实是一样的,有时候我们会通过 import 的方式引入,比如这样:
<template>
<img :src=”UserIcon”>
</template>
<script>
import UserIcon from ‘@/assets/images/user_icon.png’
export default {
data () {
return {
UserIcon
}
}
}
</script>
这样会导致打包后的代码,图片是 base64 形式(很长的一段字符串)存放在 data 中,不利于精简 data。同时当该组件多个地方使用时,每个组件实例都会携带这一段很长的 base64 代码,进一步导致数据的冗余。
因此,建议将静态图片放到 static 目录下,这样引用:
<template>
<img src=”/static/images/user_icon.png”>
</template>
代码也更简洁清爽。
看一下做了上面操作的前后对比图,使用体验上也流畅了很多。
三、swiper 优化
小程序自身提供的 swiper 组件性能上不是很好,使用时要注意。参考着两个思路:

【优化】解决 swiper 渲染很多图片时的卡顿
想请教一下小程序 swiper 组件的问题

在我使用时,由于需求原因,动态删掉 swiper-item 的思路不可行(手滑时会造成抖动)。因此只能作罢。但仍然可以优化一下:
将未显示的 swiper-item 中的图片用 v -if 隐藏到,当判断到 current 时才显示,防止大量图片的渲染导致的性能问题。
四、vuex 使用注意事项
我之前写过的一篇 mpvue 开发音频类小程序踩坑和建议里面有讲如何在小程序中使用 vuex。但遇到了个比较严重的性能问题。
1. 问题描述
我开发的是一个音频类的小程序,所以需要将播放列表 playList, 当前索引 currentIndex 和当前时长 currentTime 放进 state.js 中:
const state = {
currentIndex: 0, // playList 当前索引
currentTime: 0, // 当前播放的进度
playList: [], // {title: ”, url: ”, singer: ”}
}
每次用户点击播放音频时,都会先加载音频的播放列表 playList, 然后播放时更新当前时长 currentTime,发现有时候播音频时整个小程序非常卡顿。
注意到,音频需每秒就得更新一次 currentTime, 即每秒就做一次 setData 操作,稍微有些卡顿是可以理解的。但我发现是播放列表数据比较多时会特别卡,比如 playList 的长度是 100 条以上时。
2. 问题原因
我开启 Vue.config._mpTrace = true 后发现一个规律:
当 palyList 数据量小时,console 显示造成的数据量更新数值比较小;当 playList 比较大时,console 显示造成的数据量更新数值比较大。
PS:我曾尝试将 playList 数据量增加到 200 条,每 500ms 的数据量更新达到 800KB 左右。
到这里基本可以确定一个事实就是:更新 state 中的任何一个字段,将导致整个 state 全量一起 setData。在我这里的例子,虽然我每次只是更新 currentTime 这个字段的值,但依然导致将 state 中的其他字段如 playList,currentIndex 都一起做了一次 setData 操作。
3. 解决问题
有两个思路:

精简 state 中保存的数据,即限制 playList 的数据不能太多,可将一些数据暂存在 storage 中

vuex 采用 Module 的写法能改善这个问题,虽然使用时命名空间造成一定的麻烦。vuex 传送门

一般情况下,推荐使用后者。我在项目中尝试使用了前者,同样能达到很好的效果,请继续看下面的分享。
五、善用 storage
1. 为什么说要善用 storage
由于小程序的内存非常宝贵,占用内存过大会非常卡顿,因此最好尽可能少的将数据放到内存中,即 vuex 存的数据要尽可能少。而小程序的 storage 支持单个 key 允许存储的最大数据长度为 1MB,所有数据存储上限为 10MB。
所以可以将一些相对取用不频繁的数据放进 storage 中,需要时再将这些数据放进内存,从而缓解内存的紧张,有点类似 Windows 中虚拟内存的概念。
2.storage 换内存的实例
这个例子讲的会有点啰嗦,真正能用到的朋友可以详细看下。
上面讲到 playList 数据量太多,播放一条音频时其实只需要最多保证 3 条数据在内存中即可,即上一首,播放中的,下一首, 我们可以将多余的播放列表存放在 storage 中。
PS: 为了保证更平滑地连续切换下一首,我们可以稍微保存多几条,比如我这里选择保存 5 条数据在 vuex 中,播放时始终保证当前播放的音频前后都有两条数据。
// 首次播放背景音频的方法
async function playAudio (audioId) {
// 拿到播放列表,此时的 playList 最多只有 5 条数据。getPlayList 方法看下面
const playList = await getPlayList(audioId)
// 当前音频在 vuex 中的 currentIndex
const currentIndex = playList.findIndex(item => item.audioId === audioId)

// 播放背景音频
this.audio = wx.getBackgroundAudioManager()
this.audio.title = playList[currentIndex].title
this.audio.src = playList[currentIndex].url

// 通过 mapActions 将播放列表和 currentIndex 更新到 vuex 中
this.updateCurrentIndex(index)
this.updatePlayList(playList)
// updateCurrentIndex 和 updatePlayList 是 vuex 写好的方法
}

// 播放音频时获取播放列表的方法,将所有数据存在 storage,然后返回当前音频的前后 2 条数据,保证最多 5 条数据
import {loadPlayList} from ‘@/api/audio’
async function getPlayList (courseId, currentAudioId) {
// 从 api 中请求得到播放列表
// loadPlayList 是 api 的方法, courseId 是获取列表的参数, 表示当前课程下的播放列表
let rawList = await loadPlayList(courseId)
// simplifyPlayList 过滤掉一些字段
const list = this.simplifyPlayList(rawList)
// 将列表存到 storage 中
wx.setStorage({
key: ‘playList’,
data: list
})
return subPlayList(list, currentAudioId)
}
重点是 subPlayList 方法,这个方法保证了拿到的播放列表是最多 5 条数据。
function subPlayList(playList, currentAudioId) {
let tempArr = […playList]
const count = 5 // 保持 vuex 中最多 5 条数据
const middle = parseInt(count / 2) // 中点的索引
const len = tempArr.length
// 如果整个原始的播放列表本来就少于 5 条数据,说明不需要裁剪,直接返回
if (len <= count) {
return tempArr
}
// 找到当前要播放的音频的所在位置
const index = tempArr.findIndex(item => item.audioId === currentAudioId)
// 截取当前音频的前后两条数据
tempArr = tempArr.splice(Math.max(0, Math.min(len – count, index – middle)), count)
return tempArr
}
tempArr.splice(Math.max(0, index – middle), count) 可能有些同学比较难理解,需要仔细琢磨一下。假设 playList 有 10 条数据:

当前音频是列表中的第 1 条(索引是 0), 截取前 5 个:playList.splice(0, 5),此时 currentAudio 在这 5 个数据的索引是 0, 没有上一首,有 4 个下一首

当前音频是列表中的第 2 条(索引是 1), 截取前 5 个:playList.splice(0, 5),此时 currentAudio 在这 5 个数据的索引是 1,有 1 个上一首,3 个下一首

当前音频是列表中的第 3 条(索引是 2), 截取前 5 个:playList.splice(0, 5),此时 currentAudio 在这 5 个数据的索引是 2,有 2 个上一首,2 个下一首

当前音频是列表中的第 4 条(索引是 3), 截取第 1 到 6 个:playList.splice(1, 5)

,此时 currentAudio 在这 5 个数据的索引是 2,有 2 个上一首,2 个下一首

当前音频是列表中的第 5 条(索引是 4),截取第 2 到 7 个:playList.splice(2, 5),此时 currentAudio 在这 5 个数据的索引是 2,有 2 个上一首,2 个下一首


当前音频是列表中的第 9 条(索引是 8),截取后 5 个:playList.splice(4, 5),此时 currentAudio 在这 5 个数据的索引是 3,有 3 个上一首,1 个下一首

当前音频是列表中的最后 1 条(索引是 9),截取后的 5 个:playList.splice(4, 5),此时 currentAudio 在这 5 个数据的索引是 4,有 4 个上一首,没有下一首

有点啰嗦,感兴趣的同学仔细琢磨下,无论当前音频在哪,都始终保证了拿到当前音频前后的最多 5 条数据。
接下来就是维护播放上一首或下一首时保证当前 vuex 中的 playList 始终是包含当前音频的前后 2 条。
播放下一首
function playNextAudio() {
const nextIndex = this.currentIndex + 1
if (nextIndex < this.playList.length) {
// 没有超出数组长度,说明在 vuex 的列表中,可以直接播放
this.audio = wx.getBackgroundAudioManager()
this.audio.src = this.playList[nextIndex].url
this.audio.title = this.playList[nextIndex].title
this.updateCurrentIndex(nextIndex)
// 当判断到已经到 vuex 的 playList 的边界了,重新从 storage 中拿数据补充到 playList
if (nextIndex === this.playList.length – 1 || nextIndex === 0) {
// 拿到只有当前音频前后最多 5 条数据的列表
const newList = getPlayList(this.playList[nextIndex].courseId, this.playList[nextIndex].audioId)
// 当前音频在这 5 条数据中的索引
const index = newList.findIndex(item => item.audioId === this.playList[nextIndex].audioId)
// 更新到 vuex
this.updateCurrentIndex(index)
this.updatePlayList(newList)
}
}
}
这里的 getPlayList 方法是上面讲过的,本来是从 api 中直接获取的,为了避免每次都从 api 直接获取,所以需要改一下,先读 storage,若无则从 api 获取:
import {loadPlayList} from ‘@/api/audio’
async function getPlayList (courseId, currentAudioId) {
// 先从缓存列表中拿
const playList = wx.getStorageSync(‘playList’)
if (playList && playList.length > 0 && courseId === playList[0].courseId) {
// 命中缓存,则从直接返回
return subPlayList(playList, currentAudioId)
} else {
// 没有命中缓存,则从 api 中获取
const list = await loadPlayList(courseId)
wx.setStorage({
key: ‘playList’,
data: list
})
return subPlayList(list, currentAudioId)
}
}
播放上一首也是同理,就不赘述了。
PS: 将 vuex 中的数据精简后,我所做的小程序在播放音频时刷其他页面已经非常流畅啦,效果非常好。
六、动画优化
这个问题在 mpvue 开发音频类小程序踩坑和建议已经讲过了,感兴趣的可以移步看一眼,这里只写下概述:

如果要使用动画,尽量用 css 动画代替 wx.createAnimation
使用 css 动画时建议开启硬件加速

最后
大致总结一下上面所讲的几个要点:

开发时打开 Vue.config._mpTrace = true。
谨慎引入第三方库,权衡收益。
添加数据到 data 中时要克制,能精简尽量精简。
图片记得要压缩,图片在显示时才渲染。
vuex 保持数据精简,必要时可先存 storage。

性能优化是一个永不止步的话题,我也还在摸索,不足之处还请大家指点和分享。
欢迎关注,会持续分享前端实战中遇到的一些问题和解决办法。

正文完
 0