mpvue效能優化實戰技巧

fengxianqi發表於2019-04-16

最近一直在折騰mpvue寫的微信小程式的效能優化,分享下實戰的過程。

先上個優化前後的圖:

mpvue效能優化實戰技巧
可以看到打包後的程式碼量從813KB減少到387KB,Audits體驗評分從BA,效果還是比較明顯的。其實這個指標說明不了什麼,而且輕易就可以做到,更重要的是優化小程式執行過程中的卡頓感,請耐心往下看。

常規優化

常規的Web端優化方法在小程式中也是適用的,而且不可忽視。

一、壓縮圖片

這一步最簡單,但是容易被忽視。在tiny上線上壓縮,然後下載替換即可。

mpvue效能優化實戰技巧
我這專案的壓縮率高達72%,可以說打包後的程式碼從813KB降到387KB大部分都是歸功於壓縮圖片了。

二、移除無用的庫

我之前在專案中使用了Vant Weapp,在static目錄下引入了整個庫,但實際上我只使用了button,field,dialog等幾個元件,實在是沒必要。

所以乾脆移除掉了,微信小程式自身提供的buttonwx.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更新的資料量。如圖:

mpvue效能優化實戰技巧
如果資料更新量很大,會明顯感覺小程式執行卡頓,反之就流暢。因此我們可以根據這個指標,逐步找出效能瓶頸並解決掉。

二、精簡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
        // ...
    },
    // ...
]
複製程式碼

假設我們在小程式中只需要用到列表中的部分欄位,如果不對資料做處理,將整個articleListsetData進去,是不明智的。

小程式官方文件: 單次設定的資料不能超過1024kB,請儘量避免一次設定過多的資料

可以看出,記憶體是很寶貴的,當articleList資料量非常大超過1M時,某些機型就會爆掉(我在iOS中遇到過很多次)。

因此,需要將介面返回的資料剔除掉不需要的,再setData,回到我們上面的articleList例子,假設我們只需要用articleIdauthor這兩個欄位,可以這樣:

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>
複製程式碼

程式碼也更簡潔清爽。

看一下做了上面操作的前後對比圖,使用體驗上也流暢了很多。

mpvue效能優化實戰技巧

三、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。

效能優化是一個永不止步的話題,我也還在摸索,不足之處還請大家指點和分享。

歡迎關注,會持續分享前端實戰中遇到的一些問題和解決辦法。

相關文章