WePY 在小程序性能調優上作出的探究

性能調優是一個亙古不變的話題,不管是在傳統H5上仍是小程序中。由於實現機制不一樣,可能致使傳統H5中的某些優化方式在小程序上並不適用。所以必須另開闢蹊徑找出適合小程序的調估方式。javascript

預先加載

原理

傳統H5中也能夠經過預加載來提高用戶體驗,但在小程序中作到這一點其實是能夠更簡單方便卻又更容易被忽視的。html

傳統H5在啓動時,page1.html 只會加載 page1.html 的頁面與邏輯代碼,當page1.html 跳轉至 page2.html 時,page1 全部的 Javascript 數據將會從內存中消失。page1 與 page2 之間的數據通訊只能經過 URL 參數傳遞或者瀏覽器的 cookie,localStorge 存儲處理。vue

小程序在啓動時,會直接加載全部頁面邏輯代碼進內存,即使 page2 可能都不會被使用。在 page1 跳轉至 page2 時,page1 的邏輯代碼 Javascript 數據也不會從內存中消失。page2 甚至能夠直接訪問 page1 中的數據。java

最簡單的驗證方式就是在 page1 中加入一個 setInterval(function () {console.log('exist')}, 1000)。傳統H5中跳轉後定時器會自動消失,小程序中跳轉後定時器仍然工做。react

小程序的這種機制差別正好能夠更好的實現預加載。一般狀況下,咱們習慣將數據拉取寫在 onLoad 事件中。可是小程序的 page1 跳轉到 page2,到 page2 的 onLoad 是存在一個 300ms ~ 400ms 的延時的。以下圖:git

圖片描述github

由於小程序的特性,徹底能夠在 page1 中預先拿取數據,而後在 page2 中直接使用數據,這樣就能夠避開 redirecting 的 300ms ~ 400ms了。以下圖:web

圖片描述json

試驗

在官方demo中加入兩個頁面:page1,page2小程序

// page1.js 點擊事件中記錄開始時間
bindTap: function () {
  wx.startTime = +new Date();
  wx.navigateTo({
    url: '../page2/page2'
  });
}


// page2.js 中假設從服務器拉取數據須要500ms
fetchData: function (cb) {
  setTimeout(function () {
    cb({a:1});
  }, 500);
},
onLoad: function () {
  wx.endTime = +new Date();
  this.fetchData(function () {
    wx.endFetch = +new Date();
    console.log('page1 redirect start -> page2 onload invoke -> fetch data complete: ' + (wx.endTime - wx.startTime) + 'ms - ' + (wx.endFetch - wx.endTime) + 'ms');
  });
}

重試10次,獲得的結果以下:

圖片描述

優化

對於上述問題,WePY 中封裝了兩種概念去解決:

  • 預加載數據
    用於 page1 主動傳遞數據給 page2,好比 page2 須要加載一份耗時很長的數據。我能夠在 page1 閒時先加載好,進入 page2 時直接就可使用。

  • 預查詢數據
    用於避免於 redirecting 延時,在跳轉時調用 page2 預查詢。

擴展了生命週期,添加了onPrefetch事件,會在 redirect 之時被主動調用。同時給onLoad事件添加了一個參數,用於接收預加載或者是預查詢的數據:

// params
// data.from: 來源頁面,page1
// data.prefetch: 預查詢數據
// data.preload: 預加載數據
onLoad (params, data) {}

預加載數據示例:

// page1.wpy 預先加載 page2 須要的數據。

methods: {
  tap () {
    this.$redirect('./page2');
  }
},
onLoad () {
  setTimeout(() => {
    this.$preload('list', api.getBigList())
  }, 3000)
}

// page2.wpy 直接從參數中拿到 page1 中預先加載的數據
onLoad (params, data) {
  data.preload.list.then((list) => render(list));
}

預查詢數據示例:

// page1.wpy 使用封裝的 redirect 方法跳轉時,會調用 page2 的 onPrefetch 方法
methods: {
  tap () {
    this.$redirect('./page2');
  }
}

// page2.wpy 直接從參數中拿到 onPrefetch 中返回的數據
onPrefetch () {
  return api.getBigList();
}
onLoad (params, data) {
  data.prefetch.then((list) => render(list));
}

數據綁定

原理

在針對數據綁定作優化時,須要先了解小程序的運行機制。由於視圖層與邏輯層的徹底分離,因此兩者之間的通訊全都依賴於 WeixinJSBridge 實現。如:

  • 開發者工具中是基於 window.postMessage

  • IOS中基於 window.webkit.messageHandlers.invokeHandler.postMessage

  • Android中基於WeixinJSCore.invokeHandler

所以數據綁定方法this.setData也如此,頻繁的數據綁定就增長了通訊的成本。再來看看this.setData究竟作了哪些事情。基於開發者工具的代碼,單步調試大體還原出完整的流程,如下是還原後的代碼:

/*
setData 主流程精簡還原,並不是完整主流程,內有註釋
*/
function setData (obj) {
    if (typeof(obj) !== 'Object') {
        console.log('類型錯誤'); // 並無預期中的return;
    }
    let type = 'appDataChange';
    
    // u.default.emit(e, this.__wxWebviewId__) 代碼還原
    let e = [type, {
                data: {data: list}, 
                options: {timestamp: +new Date()}
            },
            [0] // this.__wxWebviewId__
    }];

    // WeixinJSBridge.publish.apply(WeixinJSBridge, e); 代碼還原
    var datalength = JSON.stringify(e.data).length;  // 第一次 JSON.stringify
    if (datalength > AppserviceMaxDataSize) { // AppserviceMaxDataSize === 1048576
        console.error('已經超過最大長度');
        return;
    }

    if (type === 'appDataChange' || type === 'pageInitData' || type === '__updateAppData') {

        // sendMsgToNW({appData: __wxAppData, sdkName: "send_app_data"}) 代碼還原
        __wxAppData = {
            'pages/page1/page1': alldata
        }
        e = { appData: __wxAppData, sdkName: "send_app_data" }
       
        var postdata = JSON.parse(JSON.stringify(e)); // 第二次 JSON.stringify 第一次 JSON.parse
        window.postMessage({
            postdata
        }, "*");
    }


    // sendMsgToNW({appData: __wxAppData, sdkName: "send_app_data"}) 代碼還原
    e = {
        eventName: type,
        data: e[1],
        webviewIds: [0],
        sdkName: 'publish'
    };

    var postdata = JSON.parse(JSON.stringify(e));  // 第三次 JSON.stringify 第二次 JSON.parse
    window.postMessage({
        postdata
    }, "*");
}

setData 運行的流程以下:

圖片描述

從上面代碼以及流程圖中能夠看出,在一次setData({a: 1})做時,會進行三次 JSON.stringify,二次JSON.parse以及兩次window.postMessage操做。而且在第一次window.postMessage時,並非單單隻處理傳遞的{a:1},而是處理當前頁面的全部 data 數據。所以可想而知每次setData操做的開銷是很是大的,只能經過減小數據量,以及減小setData操做來規避。

setData 相近的是 React 的 setState 方法,一樣是使用 setState 去更新視圖的,能夠經過源碼 React:L199 看到 setState 的關鍵代碼以下:

function enqueueUpdate(component) {
  ensureInjected();
  if (!batchingStrategy.isBatchingUpdates) {
    batchingStrategy.batchedUpdates(enqueueUpdate, component);
    return;
  }
  dirtyComponents.push(component);
}

setState的工做流程以下:

圖片描述

能夠看出,setState 加入了一個緩衝列隊,在同一執行流程中進行屢次 setState 以後也不會重複渲染視圖,這就是一種很好的優化方式。

實驗

爲了證明setData的性能問題,能夠寫簡單的測試例子去測試:

動態綁定1000條數據的列表進行性能測試,這裏測試了三種狀況:

  • 最優綁定: 在內存中添加完畢後最後執行setData操做。

  • 最差綁定: 在添加一條記錄執行一次setData操做。

  • 最智能綁定:無論中間進行了什麼操做,在運行結束時執行一次髒檢查,對須要設置的數據進行setData操做。

參考代碼以下:

// page1.wxml
<view bindtap="worse">
  <text class="user-motto">worse數據綁定測試</text>
</view>
<view bindtap="best">
  <text class="user-motto">best數據綁定測試</text>
</view>
<view bindtap="digest">
  <text class="user-motto">digest數據綁定測試</text>
</view>

<view class="list">
  <view wx:for="{{list}}" wx:for-index="index"wx:for-item="item">
      <text>{{item.id}}</text>---<text>{{item.name}}</text>
  </view>
</view>


// page1.js
worse: function () {
   var start = +new Date();
   for (var i = 0; i < 1000; i++) {
     this.data.list.push({id: i, name: Math.random()});
     this.setData({list: this.data.list});
   }
   var end = +new Date();
   console.log(end - start);
},
best: function () {
  var start = +new Date();
  for (var i = 0; i < 1000; i++) {
    this.data.list.push({id: i, name: Math.random()});
  }
  this.setData({list: this.data.list});
  var end = +new Date();
  console.log(end - start);
},
digest: function () {
  var start = +new Date();
  for (var i = 0; i < 1000; i++) {
    this.data.list.push({id: i, name: Math.random()});
  }
  var data = this.data;
  var $data = this.$data;
  var readyToSet = {};
  for (k in data)  {
    if (!util.$isEqual(data[k], $data[k])) {
      readyToSet[k] = data[k];
      $data[k] = util.$copy(data[k], true);
    }
  }
  if (Object.keys(readyToSet).length) {
    this.setData(readyToSet);
  }
  var end = +new Date();
  console.log(end - start);
},
onLoad: function () {
  this.$data = util.$copy(this.data, true);
}

在通過十次刷新運行測試後得出如下結果:

worse(ms) best(ms) digest(ms)
8540 24 23
7784 22 25
7884 23 25
8317 22 25
7968 28 26
7939 21 23
7853 22 23
8053 25 23
8007 24 29
8168 25 24

實現一樣的邏輯,性能數據卻相差40倍左右。由此能夠看出,在開發過程當中,必定要避免同一流程內屢次 setData 操做。

優化

在開發時,避免在同一流程內屢次使用setData固然是最佳實踐。採起人工維護確定是可以實現的,就比如能用原生 js 能寫出比衆多框架更高效的性能同樣。但當頁面邏輯負責起來以後,花很大的精力去維護都不必定能保證每一個流程只存在一次setData,並且可維護性也不高。所以,WePY選擇使用髒檢查去作數據綁定優化。用戶不用再擔憂在個人流程裏,數據被修改了多少次,只會在流程最後作一次髒檢查,而且按需執行setData

髒檢測機制借鑑自AngularJS,多數人一聽到髒檢查都會以爲是低效率的一種做法,認爲使用 Vue.js 中的 getter,setter更高效。其實否則,兩種機制都是對同一件事的不一樣實現方式。各有優劣,取決於使用的人在使用過程當中是否正好放大了機制中的劣勢面。

WePY 中的 setData 就比如是一個 setter,在每次調用時都會去渲染視圖。所以若是再封裝一層 getter、setter 就徹底沒有意義,沒有任何優化可言。這也就是爲何一個類 Vue.js 的小程序框架卻選擇了與之相反的另一種數據綁定方式。

再回來看髒檢查的問題在哪裏,從上面實驗的代碼能夠看出,髒檢查的性能問題在於每次進行髒檢查時,須要遍歷因此數據而且做值的深比較,性能取決於遍歷以及比較數據的大小。WePY 中深比較是使用的 underscore 的 isEqual 方法。爲了驗證效率問題,使用不一樣的比較方法對一個 16.7 KB 的複雜 JSON 數據進行深比較,測試用例請看這裏:deep-compare-test-case

獲得的結果以下:

圖片描述

從結果來看,對於一個 16.7 KB 的數據深比較是徹底不足以產生性能問題的。那 AngularJS 1.x 髒檢查的性能問題是怎麼出現的呢?

AngularJS 1.x 中沒有組件的概念,頁面數據就位於 controller 的 &dollar;scope 當中。每一次髒檢查都是從 &dollar;rootScope 開始,隨後遍歷至全部子 &dollar;scope。參考這裏 angular.js:L1081。對於一個大型的單頁應用來講,全部 &dollar;scope 中的數據可能達到了上百甚至上千個都有可能。那時,髒檢查的每次遍歷就可能真的會成爲了性能的瓶頸了。

反觀 WePY,使用相似於 Vue.js 的組件化開發,在拋開父子組件雙向綁定通訊的狀況下,組件的髒檢查僅針對組件自己的數據進行,一個組件的數據一般不會太多,數據太多時能夠細化組件劃分的粒度。所以在這種狀況下,髒檢查並不會致使性能問題。

其實,在不少狀況下,框架封裝的解決方案都不是性能優化的最優解決方案,使用原生確定能優化出更快的代碼。但它們之因此存在而且有價值,那都是由於它們是在性能、開發效率、可維護性上尋找到一個平衡點,這也是爲何 WePY 選擇使用髒檢查做爲數據綁定的優化。

其它優化

除了以上兩點是基於性能上作出的優化之外,WePY 也做出了一系列開發效率上的優化。由於在我以前的文章裏都有詳細說明,因此在這裏就簡單列舉一下,不作深刻探討。詳情能夠參看 WePY 文檔。

組件化開發

支持組件循環、嵌套,支持組件 Props 傳值,組件事件通訊等等。

parent.wpy
<child :item.sync="myitem" />

<repeat for="{{list}}" item="item" index="index">
   <item :item="item" />
</repeat>

支持豐富的編譯器

js 能夠選擇用 Babel 或者 TypeScript 編譯。
wxml 能夠選擇使用 Pug(原Jade)。
wxss 能夠選擇使用 Less、Sass、Styus。

支持豐富的插件處理

能夠經過配置插件對生成的js進行壓縮混淆,壓縮圖片,壓縮 wxml 和 json 已節省空間等等。

支持 ESLint 語法檢查

添加一行配置就能夠支持 ESLint 語法檢查,能夠避免低級語法錯誤以及統一項目代碼的風格。

生命週期優化

添加了 onRoute 的生命週期。用於頁面跳轉後觸發。
由於並不存在一個頁面跳轉事件(onShow 事件能夠用做頁面跳轉事件,但同時也存在負做用,好比按 HOME 鍵後切回來,或者拉起支付後取消,拉起分享後取消都會觸發 onShow 事件)。

支持 Mixin 混合

能夠靈活的進行不一樣組件之間的相同功能的複用。參考 Vue.js 官方文檔: 混合

優化事件,支持自定義事件

bindtap="tap" 簡寫爲 @tap="tap"catchtap="tap"簡寫爲@tap.stop="tap"
對於組件還提供組件自定義事件

<child @myevent.user="someevent" />

優化事件傳參

官方版本以下:

<view data-alpha-beta="1" data-alphaBeta="2" bindtap="bindViewTap"> DataSet Test </view>
Page({
  bindViewTap:function(event){
    event.target.dataset.alphaBeta === 1 // - 會轉爲駝峯寫法
    event.target.dataset.alphabeta === 2 // 大寫會轉爲小寫
  }
})

優化後:

<view @tap="bindViewTap("1", "2")"> DataSet Test </view>

methods: {
  bindViewTap(p1, p2, event) {
    p1 === "1";
    p2 === "2";
  }
}

結束語:小程序還存在不少值得開發者去探索優化的地方,歡迎你們與我探討交流開發心得。若本文存在不許確的地方,歡迎批評指正。

相關文章
相關標籤/搜索