小程式框架執行時效能大測評

滴滴WebApp架構組發表於2020-04-03

作者:董巨集平(hiyuki),滴滴出行小程式負責人,mpx框架負責人及核心作者

隨著小程式在商業上的巨大成功,小程式開發在國內前端領域越來越受到重視,為了方便廣大開發者更好地進行小程式開發,各類小程式框架也層出不窮,呈現出百花齊放的態勢。但是到目前為止,業內一直沒有出現一份全面、詳細、客觀、公正的小程式框架測評報告,為小程式開發者在技術選型時提供參考。於是我便籌劃推出一系列文章,對業內流行的小程式框架進行一次全方位的、客觀公正的測評,本文是系列文章的第一篇——執行時效能篇。

在本文中,我們會對下列框架進行執行時效能測試(排名不分先後):

其中對於kbone和taro next均以vue作為業務框架進行測試。

執行時效能的測試內容包括以下幾個維度:

  • 框架執行時體積
  • 頁面渲染耗時
  • 頁面更新耗時
  • 區域性更新耗時
  • setData呼叫次數
  • setData傳送資料大小

框架效能測試demo全部存放於 github.com/hiyuki/mp-f… 中,歡迎廣大開發者進行驗證糾錯及補全;

測試方案

為了使測試結果真實有效,我基於常見的業務場景構建了兩種測試場景,分別是動態測試場景和靜態測試場景。

動態測試場景

動態測試中,檢視基於資料動態渲染,靜態節點較少,檢視更新耗時和setData呼叫情況是該測試場景中的主要測試點。

動態測試demo模擬了實際業務中常見的長列表+多tab場景,該demo中存在兩份優惠券列表資料,一份為可用券資料,另一份為不可用券資料,其中同一時刻檢視中只會渲染展示其中一份資料,可以在上方的操作區模擬對列表資料的各種操作及檢視展示切換(切tab)。

小程式框架執行時效能大測評

動態測試demo

在動態測試中,我在外部通過函式代理的方式在初始化之前將App、Page和Component構造器進行代理,通過mixin的方式在Page的onLoad和Component的created鉤子中注入setData攔截邏輯,對所有頁面和元件的setData呼叫進行監聽,並統計小程式的檢視更新耗時及setData呼叫情況。該測試方式能夠做到對框架程式碼的零侵入,能夠跟蹤到小程式全量的setData行為並進行獨立的耗時計算,具有很強的普適性,程式碼具體實現可以檢視 github.com/hiyuki/mp-f…

靜態測試場景

靜態測試模擬業務中靜態頁面的場景,如運營活動和文章等頁面,頁面內具備大量的靜態節點,而沒有資料動態渲染,初始ready耗時是該場景下測試的重心。

靜態測試demo使用了我去年發表的一篇技術文章的html程式碼進行小程式適配構建,其中包含大量靜態節點及文字內容。

小程式框架執行時效能大測評

靜態測試demo

測試流程及資料

以下所有耗時類的測試資料均為微信小程式中真機進行5次測試計算平均值得出,單位均為ms。Ios測試環境為手機型號iPhone 11,系統版本13.3.1,微信版本7.0.12,安卓測試環境為手機型號小米9,系統版本Android10,微信版本7.0.12。

為了使資料展示不過於混亂複雜,文章中所列的資料以Ios的測試結果為主,安卓測試結論與Ios相符,整體耗時比Ios高3~4倍左右,所有的原始測試資料存放在 github.com/hiyuki/mp-f…

由於transform-runtime引入的core-js會對框架的執行時體積和執行耗時帶來一定影響,且不是所有的框架都會在編譯時開啟transform-runtime,為了對齊測試環境,下述測試均在transform-runtime關閉時進行。

框架執行時體積

由於不是所有框架都能夠使用webpack-bundle-analyzer得到精確的包體積佔用,這裡我通過將各框架生成的demo專案體積減去native編寫的demo專案體積作為框架的執行時體積。

demo總體積(KB) 框架執行時體積(KB)
native 27 0
wepy2 66 39
uniapp 114 87
mpx 78 51
chameleon 136 109
mpvue 103 76
kbone 395 368
taro next 183 156

該項測試的結論為:
native > wepy2 > mpx > mpvue > uniapp > chameleon > taro next > kbone

結論分析:

  • wepy2和mpx在框架執行時體積上控制得最好;
  • taro next和kbone由於動態渲染的特性,在dist中會生成遞迴渲染模板/元件,所以佔用體積較大。

頁面渲染耗時(動態測試)

我們使用重新整理頁面操作觸發頁面重新載入,對於大部分框架來說,頁面渲染耗時是從觸發重新整理操作到頁面執行onReady的耗時,但是對於像kbone和taro next這樣的動態渲染框架,頁面執行onReady並不代表檢視真正渲染完成,為此,我們設定了一個特殊規則,在頁面onReady觸發的1000ms內,在沒有任何操作的情況下出現setData回撥時,以最後觸發的setData回撥作為頁面渲染完成時機來計算真實的頁面渲染耗時,測試結果如下:

頁面渲染耗時
native 60.8
wepy2 64
uniapp 56.4
mpx 52.6
chameleon 56.4
mpvue 117.8
kbone 98.6
taro next 89.6

該項測試的耗時並不等同於真實的渲染耗時,由於小程式自身沒有提供performance api,真實渲染耗時無法通過js準確測試得出,不過從得出的資料來看該項資料依然具備一定的參考意義。

該項測試的結論為:
mpx ≈ chameleon ≈ uniapp ≈ native ≈ wepy2 > taro next ≈ kbone ≈ mpvue

結論分析:

  • 由於mpvue全量在頁面進行渲染,kbone和taro next採用了動態渲染技術,頁面渲染耗時較長,其餘框架並無太大區別。

頁面更新耗時(無後臺資料)

這裡後臺資料的定義為data中存在但當前頁面渲染中未使用到的資料,在這個demo場景下即為不可用券的資料,當前會在不可用券為0的情況下,對可用券列表進行各種操作,並統計更新耗時。

更新耗時的計算方式是從資料操作事件觸發開始到對應的setData回撥完成的耗時

mpvue中使用了當前時間戳(new Date)作為超時依據對setData進行了超時時間為50ms的節流操作,該方式存在嚴重問題,當vue內單次渲染同步流程執行耗時超過50ms時,後續元件patch觸發的setData會突破這個節流限制,以50ms每次的頻率對setData進行高頻無效呼叫。在該效能測試demo中,當優惠券數量超過500時,介面就會完全卡死。為了順利跑完整個測試流程,我對該問題進行了簡單修復,使用setTimeout重寫了節流部分,確保在vue單次渲染流程同步執行完畢後才會呼叫setData傳送合併資料,之後mpvue的所有效能測試都是基於這個patch版本來進行的,該patch版本存放在https://github.com/hiyuki/mp-framework-benchmark/blob/master/frameworks/mpvue/runtime/patch/index.js

理論上來講native的效能在進行優化的前提下一定是所有框架的天花板,但是在日常業務開發中我們可能無法對每一次setData都進行優化,以下效能測試中所有的native資料均採用修改資料後全量傳送的形式來實現。

第一項測試我們使用新增可用券(100)操作將可用券數量由0逐級遞增到1000:

100 200 300 400 500 600 700 800 900 1000
native 84.6 69.8 71.6 75 77.2 78.8 82.8 93.2 93.4 105.4
wepy2 118.4 168.6 204.6 246.4 288.6 347.8 389.2 434.2 496 539
uniapp 121.2 100 96 98.2 97.8 99.6 104 102.4 109.4 107.6
mpx 110.4 87.2 82.2 83 80.6 79.6 86.6 90.6 89.2 96.4
chameleon 116.8 115.4 117 119.6 122 125.2 133.8 133.2 144.8 145.6
mpvue 112.8 121.2 140 169 198.8 234.2 278.8 318.4 361.4 408.2
kbone 556.4 762.4 991.6 1220.6 1468.8 1689.6 1933.2 2150.4 2389 2620.6
taro next 470 604.6 759.6 902.4 1056.2 1228 1393.4 1536.2 1707.8 1867.2

然後我們按順序逐項點選刪除可用券(all) > 新增可用券(1000) > 更新可用券(1) > 更新可用券(all) > 刪除可用券(1)

delete(all) add(1000) update(1) update(all) delete(1)
native 32.8 295.6 92.2 92.2 83
wepy2 56.8 726.4 49.2 535 530.8
uniapp 43.6 584.4 54.8 144.8 131.2
mpx 41.8 489.6 52.6 169.4 165.6
chameleon 39 765.6 95.6 237.8 144.8
mpvue 103.6 669.4 404.4 414.8 433.6
kbone 120.2 4978 2356.4 2419.4 2357
taro next 126.6 3930.6 1607.8 1788.6 2318.2

該項測試中初期我update(all)的邏輯是迴圈對每個列表項進行更新,形如listData.forEach((item)=>{item.count++}),發現在chameleon框架中執行介面會完全卡死,追蹤發現chameleon框架中沒有對setData進行非同步合併處理,而是在資料變動時直接同步傳送,這樣在資料量為1000的場景下用該方式進行更新會高頻觸發1000次setData,導致介面卡死;對此,我在chameleon框架的測試demo中,將update(all)的邏輯調整為深clone產生一份更新後的listData,再將其整體賦值到this.listData當中,以確保該項測試能夠正常進行。

該項測試的結論為:
native > mpx ≈ uniapp > chameleon > mpvue > wepy2 > taro next > kbone

結論分析:

  • mpx和uniapp在框架內部進行了完善的diff優化,隨著資料量的增加,兩個框架的新增耗時沒有顯著上升;
  • wepy2會在資料變更時對props資料也進行setData,在該場景下造成了大量的無效效能損耗,導致效能表現不佳;
  • kbone和taro next採用了動態渲染方案,每次新增更新時會傳送大量描述dom結構的資料,與此同時動態遞迴渲染的耗時也遠大於常規的靜態模板渲染,使得這兩個框架在所有的更新場景下耗時都遠大於其他框架。

頁面更新耗時(有後臺資料)

重新整理頁面後我們使用新增不可用券(1000)建立後臺資料,觀察該操作是否會觸發setData並統計耗時

back add(1000)
native 45.2
wepy2 174.6
uniapp 89.4
mpx 0
chameleon 142.6
mpvue 134
kbone 0
taro next 0

mpx進行setData優化時inspired by vue,使用了編譯時生成的渲染函式跟蹤模板資料依賴,在後臺資料變更時不會進行setData呼叫,而kbone和taro next採用了動態渲染技術模擬了web底層環境,在上層完整地執行了vue框架,也達到了同樣的效果。

然後我們執行和上面無後臺資料時相同的操作進行耗時統計,首先是遞增100:

100 200 300 400 500 600 700 800 900 1000
native 88 69.8 71.2 80.8 79.4 84.4 89.8 93.2 99.6 108
wepy2 121 173.4 213.6 250 298 345.6 383 434.8 476.8 535.6
uniapp 135.4 112.4 110.6 106.4 109.6 107.2 114.4 116 118.8 117.4
mpx 112.6 86.2 84.6 86.8 90 87.2 91.2 88.8 92.4 93.4
chameleon 178.4 178.2 186.4 184.6 192.6 203.8 210 217.6 232.6 236.8
mpvue 139 151 173.4 194 231.4 258.8 303.4 340.4 384.6 429.4
kbone 559.8 746.6 980.6 1226.8 1450.6 1705.4 1927.2 2154.8 2367.8 2617
taro next 482.6 626.2 755 909.6 1085 1233.2 1384 1568.6 1740.6 1883.8

然後按下表操作順序逐項點選統計

delete(all) add(1000) update(1) update(all) delete(1)
native 43.4 299.8 89.2 89 87.2
wepy2 43.2 762.4 50 533 522.4
uniapp 57.8 589.8 62.6 160.6 154.4
mpx 45.8 490.8 52.8 167 166
chameleon 93.8 837 184.6 318 220.8
mpvue 124.8 696.2 423.4 419 430.6
kbone 121.4 4978.2 2331.2 2448.4 2348
taro next 129.8 3947.2 1610.4 1813.8 2290.2

該項測試的結論為:
native > mpx > uniapp > chameleon > mpvue > wepy2 > taro next > kbone

結論分析:

  • 具備模板資料跟蹤能力的三個框架mpx,kbone和taro next在有後臺資料場景下耗時並沒有顯著增加;
  • wepy2當中的diff精度不足,耗時也沒有產生明顯變化;
  • 其餘框架由於每次更新都會對後臺資料進行deep diff,耗時都產生了一定提升。

頁面更新耗時(大資料量場景)

由於mpvue和taro next的渲染全部在頁面中進行,而kbone的渲染方案會額外新增大量的自定義元件,這三個框架都會在優惠券數量達到2000時崩潰白屏,我們排除了這三個框架對其餘框架進行大資料量場景下的頁面更新耗時測試

首先還是在無後臺資料場景下使用新增可用券(1000)將可用券數量遞增至5000:

1000 2000 3000 4000 5000
native 332.6 350 412.6 498.2 569.4
wepy2 970.2 1531.4 2015.2 2890.6 3364.2
uniapp 655.2 593.4 655 675.6 718.8
mpx 532.2 496 548.6 564 601.8
chameleon 805.4 839.6 952.8 1086.6 1291.8

然後點選新增不可用券(5000)將後臺資料量增加至5000,再測試可用券數量遞增至5000的耗時:

back add(5000)
native 117.4
wepy2 511.6
uniapp 285
mpx 0
chameleon 824
1000 2000 3000 4000 5000
native 349.8 348.4 430.4 497 594.8
wepy2 1128 1872 2470.4 3263.4 4075.8
uniapp 715 666.8 709.2 755.6 810.2
mpx 538.8 501.8 562.6 573.6 595.2
chameleon 1509.2 1672.4 1951.8 2232.4 2586.2

該項測試的結論為:
native > mpx > uniapp > chameleon > wepy2

結論分析:

  • 在大資料量場景下,框架之間基礎效能的差異會變得更加明顯,mpx和uniapp依然保持了接近原生的良好效能表現,而chameleon和wepy2則產生了比較顯著的效能劣化。

區域性更新耗時

我們在可用券數量為1000的情況下,點選任意一張可用券觸發選中狀態,以測試區域性更新效能

toggleSelect(ms)
native 2
wepy2 2.6
uniapp 2.8
mpx 2.2
chameleon 2
mpvue 289.6
kbone 2440.8
taro next 1975

該項測試的結論為:
native ≈ chameleon ≈ mpx ≈ wepy2 ≈ uniapp > mpvue > taro next > kbone

結論分析:

  • 可以看出所有使用了原生自定義元件進行元件化實現的框架區域性更新耗時都極低,這足以證明小程式原生自定義元件的優秀性和重要性;
  • mpvue由於使用了頁面更新,區域性更新耗時顯著增加;
  • kbone和taro next由於遞迴動態渲染的效能開銷巨大,導致區域性更新耗時同樣巨大。

setData呼叫

我們將proxySetData的count和size選項設定為true,開啟setData的次數和體積統計,重新構建後按照以下流程執行系列操作,並統計setData的呼叫次數和傳送資料的體積。

操作流程如下:

  1. 100逐級遞增可用券(0->500)
  2. 切換至不可用券
  3. 新增不可用券(1000)
  4. 100逐級遞增可用券(500->1000)
  5. 更新可用券(all)
  6. 切換至可用券

操作完成後我們使用getCountgetSize方法獲取累積的setData呼叫次數和資料體積,其中資料體積計算方式為JSON.stringify後按照utf-8編碼方式進行體積計算,統計結果為:

count size(KB)
native 14 803
wepy2 3514 1124
mpvue 16 2127
uniapp 14 274
mpx 8 261
chameleon 2515 319
kbone 22 10572
taro next 9 2321

該項測試的結論為:
mpx > uniapp > native > chameleon > wepy2 > taro next > mpvue > kbone

結論分析:

  • mpx框架成功實現了理論上setData的最優;
  • uniapp由於缺失模板追蹤能力緊隨其後;
  • chameleon由於元件每次建立時都會進行一次不必要的setData,產生了大量無效setData呼叫,但是資料的傳送本身經過diff,在資料傳送量上表現不錯;
  • wepy2的元件會在資料更新時呼叫setData傳送已經更新過的props資料,因此也產生了大量無效呼叫,且diff精度不足,傳送的資料量也較大;
  • taro next由於上層完全基於vue,在資料傳送次數上控制到了9次,但由於需要傳送大量的dom描述資訊,資料傳送量較大;
  • mpvue由於使用較長的資料路徑描述資料對應的元件,也產生了較大的資料傳送量;
  • kbone對於setData的呼叫控制得不是很好,在上層執行vue的情況依然進行了22次資料傳送,且傳送的資料量巨大,在此流程中達到了驚人的10MB。

頁面渲染耗時(靜態測試)

此處的頁面渲染耗時與前面描述的動態測試場景中相同,測試結果如下:

頁面渲染耗時
native 70.4
wepy2 86.6
mpvue 115.2
uniapp 69.6
mpx 66.6
chameleon 65
kbone 144.2
taro next 119.8

該項測試的結論為:
chameleon ≈ mpx ≈ uniapp ≈ native > wepy2 > mpvue ≈ taro next > kbone

結論分析:

  • 除了kbone和taro next採用動態渲染耗時增加,mpvue使用頁面模板渲染效能稍差,其餘框架的靜態頁面渲染表現都和原生差不多。

結論

綜合上述測試資料,我們得到最終的小程式框架執行時效能排名為:
mpx > uniapp > chameleon > wepy2 > mpvue > taro next > kbone

一點私貨

雖然kbone和taro next採用了動態渲染技術在效能表現上並不盡如人意,但是我依然認為這是很棒的技術方案。雖然本文從頭到位都在進行效能測試和對比,但效能並不是框架的全部,開發效率和高可用性仍然是框架的重心,開發效率相信是所有框架設計的初衷,但是高可用性卻在很大程度被忽視。從這個角度來說,kbone和taro next是非常成功的,不同於過去的轉譯思路,這種從抹平底層渲染環境的做法能夠使上層web框架完整執行,在框架可用性上帶來非常大的提升,非常適合於運營類簡單小程式的遷移和開發。

我主導開發的mpx框架(github.com/didi/mpx) 選擇了另一條道路解決可用性問題,那就是基於小程式原生語法能力進行增強,這樣既能避免轉譯web框架時帶來的不確定性和不穩定性,同時也能帶來非常接近於原生的效能表現,對於複雜業務小程式的開發者來說,非常推薦使用。在跨端輸出方面,mpx目前能夠完善支援業內全部小程式平臺和web平臺的同構輸出,滴滴內部最重要最複雜的小程式——滴滴出行小程式完全基於mpx進行開發,並利用框架提供的跨端能力對微信和支付寶入口進行同步業務迭代,大大提升了業務開發效率。

相關文章