做者:龔澄javascript
性能調優是一個亙古不變的話題,不管是在傳統H5上仍是小程序中。由於實現機制不一樣,可能致使傳統H5中的某些優化方式在小程序上並不適用。所以必須另開闢蹊徑找出適合小程序的調估方式。html
本文旨在介紹兩點在小程序開發過程中碰到的一些性能問題以及 WePY 的一些優化方案。java
小程序組件化框架 WePY 介紹請閱讀:《打造「微信小程序」組件化開發框架》web
傳統H5中也能夠經過預加載來提高用戶體驗,但在小程序中作到這一點其實是能夠更簡單方便卻又更容易被忽視的。json
傳統H5在啓動時,page1.html 只會加載 page1.html 的頁面與邏輯代碼,當page1.html 跳轉至 page2.html 時,page1 全部的 Javascript 數據將會從內存中消失。page1 與 page2 之間的數據通訊只能經過 URL 參數傳遞或者瀏覽器的 cookie,localStorge 存儲處理。小程序
小程序在啓動時,會直接加載全部頁面邏輯代碼進內存,即使 page2 可能都不會被使用。在 page1 跳轉至 page2 時,page1 的邏輯代碼 Javascript 數據也不會從內存中消失。page2 甚至能夠直接訪問 page1 中的數據。微信小程序
最簡單的驗證方式就是在 page1 中加入一個 setInterval(function () {console.log('exist')}, 1000)
。傳統H5中跳轉後定時器會自動消失,小程序中跳轉後定時器仍然工做。api
小程序的這種機制差別正好能夠更好的實現預加載。一般狀況下,咱們習慣將數據拉取寫在 onLoad 事件中。可是小程序的 page1 跳轉到 page2,到 page2 的 onLoad 是存在一個 300ms ~ 400ms 的延時的。以下圖:瀏覽器
由於小程序的特性,徹底能夠在 page1 中預先拿取數據,而後在 page2 中直接使用數據,這樣就能夠避開 redirecting 的 300ms ~ 400ms了。以下圖:性能優化
在官方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 中封裝了兩種概念去解決:
// 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
window.webkit.messageHandlers.invokeHandler.postMessage
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); }
在通過十次刷新運行測試後得出如下結果:
實現一樣的邏輯,性能數據卻相差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 (https://jsperf.com/deep-compare/4)
獲得的結果以下:
從結果來看,對於一個 16.7 KB 的數據深比較是徹底不足以產生性能問題的。那 AngularJS 1.x 髒檢查的性能問題是怎麼出現的呢?
AngularJS 1.x 中沒有組件的概念,頁面數據就位於 controller 的 $scope 當中。每一次髒檢查都是從 $rootScope 開始,隨後遍歷至全部子 $scope。參考這裏 angular.js:L1081。對於一個大型的單頁應用來講,全部 $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 語法檢查,能夠避免低級語法錯誤以及統一項目代碼的風格。
添加了 onRoute 的生命週期。用於頁面跳轉後觸發。
由於並不存在一個頁面跳轉事件(onShow 事件能夠用做頁面跳轉事件,但同時也存在負做用,好比按 HOME 鍵後切回來,或者拉起支付後取消,拉起分享後取消都會觸發 onShow 事件)。
能夠靈活的進行不一樣組件之間的相同功能的複用。參考 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"; } }
小程序還存在不少值得開發者去探索優化的地方,歡迎你們與我探討交流開發心得。若本文存在不許確的地方,歡迎批評指正。
更多精彩內容歡迎關注騰訊 Bugly的微信公衆帳號:
騰訊 Bugly是一款專爲移動開發者打造的質量監控工具,幫助開發者快速,便捷的定位線上應用崩潰的狀況以及解決方案。智能合併功能幫助開發同窗把天天上報的數千條 Crash 根據根因合併分類,每日日報會列出影響用戶數最多的崩潰,精準定位功能幫助開發同窗定位到出問題的代碼行,實時上報能夠在發佈後快速的瞭解應用的質量狀況,適配最新的 iOS, Android 官方操做系統,鵝廠的工程師都在使用,快來加入咱們吧!