GMTC全球大前端技術大會,DCloud深度剖析小程序架構及性能優化

序言:由極客邦科技旗下InfoQ中國主辦的 GMTC 全球大前端技術大會(2019 · 深圳站)於12 月 20 日成功開幕,DCloud CTO 崔紅保出席大會,並作了《小程序的將來方向》的專題演講,會上崔紅保對小程序架構進行了深度剖析,並分析了由此架構引起的性能坑點及對應的優化方案,本文是對應的文字版,分享給你們,Enjoy~前端

分享大綱

瞭解引擎架構,才能對性能優化有更多的瞭解。本議題將深度剖析小程序架構,闡述這種架構的優勢以及必然伴隨帶來的缺陷,提供了突破性能瓶頸的方案。node

小程序架構

這是一個比較通用的小程序架構,目前幾家小程序架構設計大體都是這樣的(快應用的區別是視圖層只有原生渲染)。react

你們知道小程序是一個邏輯、視圖層分離的架構。web

邏輯層就是上圖左上角這塊,小程序中開發的全部頁面JS代碼,最後都會打包合併到邏輯層,邏輯層除了執行開發者的業務JS代碼外,還需處理小程序框架的內置邏輯,好比App生命週期管理。express

視圖層就是上圖右上角這塊,用戶可見的UI效果、可觸發的交互事件在視圖層完成,視圖層包含web組件、原生組件兩種,也就是小程序是原生+web混合渲染的模式,這塊後面會詳細講。canvas

邏輯層最後運行在JS CORE或V8環境中;JS CORE既不是瀏覽器環境,也不是node環境,你是沒法使用JS中的window、DOM對象,你能調用的僅僅是ECMAScript標準規範中所給出的方法。小程序

那若是你要發送網絡請求怎麼辦?window.XMLHttpRequest 是沒法使用的(固然即便能夠調用,在iOS的WKWebView中也存在更嚴格的跨域限制,會有問題)。這時候,網絡請求就須要經過原生的網絡模塊來發送,JS CORE和原生之間呢,就須要這個JS Bridge來通信。微信小程序

架構引起的性能坑點

小程序這個架構最大的好處是新頁面加載能夠並行,讓頁面加載更快,且不卡轉場動畫。固然有的小程序引擎沒有用好,致使新頁面加載常常白屏,微信小程序仍是足夠快和穩定的。跨域

但這樣的架構設計,其實也引起了很多性能坑點,今天主要分享3點:瀏覽器

邏輯層/視圖層通信阻塞

咱們從swipeAction這個例子講起,需求是用戶在列表項上向左滑動,右側隱藏的菜單跟隨用戶手勢平滑移動

若想在小程序架構上實現流暢的跟手滑動,是很困難的,爲何?

咱們再回顧一下上面的小程序架構,小程序的運行環境分爲邏輯層和視圖層,分別由2個線程管理,小程序在視圖層與邏輯層兩個線程間提供了數據傳輸和事件系統。這樣的分離設計,帶來了顯而易見的好處:

環境隔離,既保證了安全性,同時也是一種性能提高的手段,邏輯和視圖分離,即便業務邏輯計算很是繁忙,也不會阻塞渲染和用戶在視圖層上的交互

但同時也帶來了明顯的壞處:

· 視圖層(webview)中不能運行開發者編寫的JS,而邏輯層JS又沒法直接修改頁面DOM,數據更新及事件系統只能靠線程間通信,但跨線程通訊的成本極高,特別是須要頻繁通訊的場景

基於這樣的架構設計,咱們回到swipeAction,分析一次touchmove的操做,小程序內部的響應過程:

· 用戶拖動列表項,視圖層觸發touchmove 事件,經Native層中轉通知邏輯層(邏輯層、視圖層不是直接通信的,需Native中轉),即下圖中的⓵、⓶兩步

· 邏輯層計算需移動的位置,而後再經過 setData 傳遞位置數據到視圖層,中間一樣會由微信客戶端(Native)作中轉,即下圖中的⓷、⓸兩步

實際上,用戶滑動過程當中,touchmove的回調觸發是很是頻繁的,每次回調都須要4個步驟的通信過程,高頻率回調致使通信成本大幅增長,極有可能致使頁面卡頓或抖動。爲何會卡頓,由於通信太過頻繁,視圖層沒法在16毫秒內完成UI更新。

爲解決這種通信阻塞的問題,各家小程序都在逐步提供對應的解決方案,好比微信的WXS、支付寶的SJS、百度的Filter,但每家小程序支持狀況不一樣,詳細見下表。

另外,微信的關鍵幀動畫、百度的animation-view Lottie動畫,也是爲減小頻繁通信的一種變動方式。

其實,通信阻塞是業界廣泛存在的一個問題,不止小程序,react native、weex等一樣存在通信阻塞的問題。只不過react native、weex的視圖層是原生渲染,而小程序是web渲染。咱們下面以weex爲例來講明。

你們知道,weex底層使用的 JS-Native Bridge,這個 Bridge 使得 JS 和 Native 之間的通訊會有固定的性能損耗。

繼續以上述swipeaction爲例,要實現列表項菜單的跟手滑動,大體需經以下流程:

· 在UI視圖上綁定 touch 事件(或 pan 事件)

· 當手勢觸發時, Native UI層將手勢事件經過 Bridge 傳遞給 JS邏輯層 , 這產生了一次 Native UI到 JS 邏輯的通訊,即下圖中的⓵、⓶兩步

· JS 邏輯在接收到事件後,根據手指移動的偏移量驅動界面變化,這又會產生一次 JS 到 Native UI的通訊,即下圖中的⓷、⓸兩步

一樣,手勢回調事件觸發的頻率是很是高的,頻繁的的通訊帶來的時間成本極可能致使界面沒法在16ms中完成繪製,卡頓也就產生了。

weex爲解決通信阻塞,提供了BindingX解決方案,這是一種稱之爲Expression Binding的機制,簡要介紹一下:

· 接收手勢事件的視圖,在移動過程當中的偏移量以x,y兩個變量表示

· 指望改變(跟隨移動)的視圖,變化的屬性爲translateX和translateY,對應變化的偏移量以f(x),f(y)表達式表示

· 將"交互行爲"以表達式的方式描述,並提早預置到Native UI層

· 交互觸發時,Native UI根據其內置的表達式解析引擎,去執行表達式,並根據表達式執行的結果驅動視圖變換,這個過程無需和JS邏輯通信

僞代碼 - 摘錄自weex官網

{
  
  anchor: foo_view.ref                   // ----> 這是"產生手勢的視圖"的引用  
  props:
          [
              {
                  element: foo_view.ref, // ----> 這是"指望改變的視圖"的引用
                  expression: f(x) = x, // ----> 這是具體的表達式
                  property: translateX   // ----> 這是指望改變的屬性
              },
              {
                  element: foo_view.ref,
                  expression: f(y) = y, // ----> y 屬性
                  property: translateY
              }
          ]
}

React Native 一樣存在相似問題,爲避免頻繁的通訊,React Native 生態也有對應方案,好比Animated組件及Lottie動畫支持。以 Animated 組件爲例,爲實現流暢的動畫效果,該組件採用了聲明式的API,在 JS 端僅定義了輸入與輸出以及具體的 transform 行爲,而真正的動畫是經過 Native Driver 在 Native 層執行,這樣就避免了頻繁的通訊。然而,聲明式的方式可以定義的行爲有限,沒法勝任交互場景。

uni-app在App端一樣面臨通信阻塞的問題,咱們目前的方案是採用相似微信wxs的機制(內部叫renderjs),但放開了wxs中沒法獲取頁面DOM元素的限制,好比下圖中多個小球同時移動的canvas動畫,uni-app在App端的實現方案是:

· renderjs 中獲取canvas對象

· 基於web的canvas繪製動畫,而非原生canvas繪製

Tips:你們須要注意,並非全部場景都是原生性能更好,小程序架構下,如上多球同時移動的動畫,原生canvas並不如在wxs(renderjs)中直接調用web canvas

下表總結了跨端框架在通信阻塞方面的解決方案。

數據/組件差量更新

小程序開發很是須要注意的就是setData的調用,由於每次setData,都是一次邏輯層向視圖層通訊的過程。開發者應儘量的:

· 減小調用setData的次數

· 每次調用setData,傳遞儘量少的數據量,即數據差量更新

減小setData調用次數

假設咱們有更改多個變量值的需求,示例以下:

change:function(){
  this.setData({a:1});
  ... //其它業務邏輯
  this.setData({b:2});
  ... //其它業務邏輯
  this.setData({c:3});
  ... //其它業務邏輯
  this.setData({d:4});
}

如上4次調用setData,會引起4次邏輯層、視圖層數據通信。這種場景,開發者需意識到setData有極高的調用代價,本身需手動調整代碼,合併數據,減小數據通信次數。

部分小程序三方框架已內置數據自動合併的能力(好比uni-app),開發者無需關心setData的調用代價,專一業務邏輯實現便可,建議你們使用。

減小setData調用次數,還有個注意點:後臺頁面(用戶不可見的頁面)應避免調用setData。

數據差量更新

假設咱們有一個 "列表頁 + 上拉加載" 的場景,初始化列表項爲 "item1 ~ item4",用戶上拉後要向列表追加4條新記錄 "item5 ~ item8",小程序代碼以下:

page({
  data:{
      list:['item1','item2','item3','item4']
  },
  change:function(){
      let newData = ['item5','item6','item7','item8'];
      this.data.list.push(...newData); //列表項新增記錄
      this.setData({
          list:this.data.list
      })
  }
})

如上代碼,change方法執行時,會將list中的 "item1 ~ item8" 8個列表項經過setData所有傳輸過去,而實際上變化的數據只有"item5 ~ item8"。

開發者在這種場景下,應經過差量計算,僅經過setData傳遞變化的數據,以下是一個示例代碼:

page({
  data:{
      list:['item1','item2','item3','item4']
  },
  change:function(){
      // 經過長度獲取下一次渲染的索引
      let index = this.data.list.length;
      let newData = ['item5','item6','item7','item8'];
      let newDataObj = {};//變化的數據
      newData.forEach((item) => {
          newDataObj['list[' + (index++) + ']'] = item;//經過list下標精確控制變動內容
      });
      this.setData(newDataObj) //設置差量數據
  }
})

每次都手動計算差量變動數據是繁瑣的,新手不理解小程序原理的話,也容易忽略這些性能點,給App埋下性能坑點。

此處建議開發者選擇成熟的小程序三方框架,這些框架已經自動封裝差量數據計算,對開發者更友好。好比uni-app借鑑了 westore JSON Diff庫,在調用setData以前,會先比對歷史數據,精確高效計算出有變化的差量數據,而後再調用setData,僅傳輸變化的數據,這樣可實現傳遞數據量的最小化,提高通信性能。以下是一個示例代碼:

export default{
  data(){
      return {
          list:['item1','item2','item3','item4']
      }
  },
  methods:{
      change:function(){
          let newData = ['item5','item6','item7','item8'];
          this.list.push(...newData) // 直接賦值,框架會自動計算差量數據
      }
  }
}

Tips:如上change方法執行時,僅會將list中的"item5 ~ item8"4個新增列表項傳輸過去,實現了setData傳輸量的極簡化

組件差量更新

下圖是一個微博列表截圖:

假設當前有200條微博,用戶對某條微博點贊,需實時變動其點贊數據(狀態);在傳統模式下,一條微博的點贊狀態變動,會將整個頁面(Page)的數據所有經過setData傳遞過去,這個消耗是很是高的。你就會發現那個點贊按鈕點下去後,要等一會才能變爲已讚的狀態。 而經過以前介紹,經過差量計算的方式獲取變動數據,這個 Diff 遍歷範圍也很大,計算效率極低。

如何實現更高性能的微博點贊?這其實就是組件更新的典型場景。

合適的方式應該是,將每一個點贊按鈕封裝成一個組件,用戶點贊後,僅在當前組件範圍內計算差量數據(可理解爲Diff範圍縮小爲原來的1/200),這樣效率纔是最高的。

提醒你們注意,並非全部小程序三方框架都已實現自定義組件,只有在基於自定義組件模式封裝的框架,性能纔會大幅提高;若是三方框架是基於老的template模板封裝的組件開發,則性能並不會有明顯改善,其 Diff 對比範圍依然是Page頁面級的。

混合渲染

你們知道,小程序當中有一類特殊的內置組件——原生組件,這類組件有別於 WebView 渲染的內置組件,他們是由原生客戶端渲染的。

小程序中的原生組件,從使用方式上來講,主要分爲三類:

· 經過配置項建立的:選項卡、導航欄,還有下拉刷新

· 經過組件名稱建立的,好比:camera、canvas、input、live-player、live-pusher、map、textarea、video

· 經過API接口建立的,好比:showModal、showActionSheet等

除了上面提到的這些以外,其它基本都是web渲染。因此說,小程序是混合渲染模式,web渲染爲主,原生渲染爲輔。

爲何要引入混合渲染

接下來的問題,爲何要引入原生渲染?以及爲何僅針對這幾個組件提供了原生加強?其餘組件爲何沒有作原生實現?

每一個解決方案的誕生,要了解它爲了解決什麼問題而出現:

· tabbar/navigationbar:避免切換頁面白屏,提高新窗口進入時的用戶體驗。雖然不使用原生的tabbar和導航欄,能夠作出更靈活的界面,但在切換頁面那短短300毫秒內,想保證頁面不白屏,仍是須要使用渲染更快的原生tabbar和導航欄。

· video:全屏後的滑動控制(聲音、進度、亮度等),更豐富的視頻格式

· map:更流暢的雙指縮放、位置拖動

· input:web端的input,鍵盤彈出時,只有完成按鈕,沒法讓鍵盤右下角顯示發送、下一個這樣的按鍵

提到input控件的原生化,能夠稍微發散一下。

小程序中原生input控件的一般作法是,未獲取焦點時以web控件顯示,但在獲取焦點時,繪製一個原生input,蓋在web input上方,此時用戶看見的鍵盤即爲原生input所對應的鍵盤,原生彈出鍵盤是可自定義按鈕(如上圖中下一步、send按鈕)的。這種作法存在一個缺陷:web和原生,畢竟不一樣渲染引擎,在鍵盤彈出和關閉時,對應input的placeholder會閃爍。

在Android平臺,還有一種作法是基於webkit改造,定製彈出鍵盤樣式;這種方案,在鍵盤彈出和關閉時,input控件都是web實現的,故不存在placeholder閃爍的問題。

混合渲染引起的問題

原生組件雖然帶來了更豐富的特性及更好的性能,但同時也引入了一些新的問題,好比:

1.層級問題:原生永遠在最高層,沒法經過z-index設置不一樣元素的層級,沒法與 view、image 等內置組件相互覆蓋,不支持在picker-view、scroll-view、swiper等組件中使用,就是沒法在前端的區域滾動組件中進行區域滾動

2.通信問題:好比一個長列表中內嵌視頻組件,頁面滾動時,需通知原生的視頻組件一塊兒滾動,通信阻塞,可能致使組件抖動或拖影

3.字體問題:在Android手機上,調整系統主題字體,全部原生渲染的控件的字體都會變化,而web渲染的字體則不會變化。以下圖,系統rom字體爲一款"你的名字"的三方字體,設置後,小程序頂部標題字體變了,底部選項卡字體也變了,但小程序中間內容區字體不變,這就是比較尷尬的一種狀況,一個頁面,兩種字體。

固然,字體問題並不是無解。各家小程序基本都是自帶一個webview內核,而不是使用系統webview,經過定製修改webview也可使用rom主題字體,好比微信、qq、支付寶;其餘小程序(百度、頭條),webview仍然沒法渲染爲rom主題字體。

混合渲染改進方案

既然混合渲染有這些問題,對應就會有解決方案,目前已有的方案以下。

方案1:創造層級更高的組件

既然其它組件沒法覆蓋到原生組件上,那就創造出一種新的組件,讓這個新組件能夠覆蓋到video或map上。cover-view/cover-image就是基於這種需求創造出來的新組件;其實它們也是原生組件,只不過層級略高,能夠覆蓋在 map、video、canvas、camera等原生組件上。

目前除了字節跳動外,其它幾家小程序均已支持cover-view/cover-image。

cover-view/cover-image 在必定程度上緩解了分層覆蓋的問題,但也有部分限制,好比嚴格的嵌套順序。

方案2:消除分層,同層渲染

既然分層有問題,那就消除分層,從2層變成1層,全部組件都在一個層中,z-index豈不就可生效了?

這個小目標提及來簡單,具體實現仍是很複雜的,下個章節具體介紹。

同層渲染

拋開小程序當前架構實現,解決混合渲染最直接的方案,應該更換渲染引擎,所有基於原生渲染,video/map和image/view均爲原生控件,層級相同,層級遮蓋問題天然消失。這正是uni-app在App端的推薦方案。

uni-app在App端支持weex原生渲染,至於uni-app如何抹平weex和小程序的各項差別,這是另一個話題,後續可單獨分享。

迴歸到當前web渲染爲主、原生渲染爲輔的主流小程序現狀,如何實現同層渲染?

基於咱們的分析研究,這裏簡單講解一下同層渲染實現的方案,和微信真實實現可能會有出入(目前僅微信一家實現了同層渲染)。

iOS平臺

小程序在 iOS 端使用 WKWebView 進行渲染,WKWebView 在內部採用的是分層的方式進行渲染,通常會將多個DOM節點,合併到一個層上進行渲染,所以DOM節點和層之間不存在一一對應關係。可是,一旦將一個 DOM 節點的 CSS 屬性設置爲 overflow: scroll 後,WKWebView 便會爲其生成一個 WKChildScrollView,且WebKit 內核已經處理了WKChildScrollView與其餘 DOM 節點之間的層級關係,這時DOM節點就和層之間有一一對應關係了。

小程序 iOS 端的同層渲染可基於 WKChildScrollView 實現,主要流程以下:

· 建立一個 DOM 節點並設置其 CSS 屬性爲 overflow: scroll

· 通知原生層查找到該 DOM 節點對應的原生 WKChildScrollView 組件

· 將原生組件掛載到該 WKChildScrollView 節點上做爲其子 View

Android平臺

小程序在 Android 端採用 chromium 做爲 WebView 渲染層,和iOS的WKWebView不一樣,是統一渲染的,不會分層渲染。但chromium 支持 WebPlugin 機制,WebPlugin 是瀏覽器內核的一個插件機制,可用來解析<embed>。Android 端的同層渲染可基於 <embed> 加 chromium 內核擴展來實現,大體流程以下:

· 原生層建立一個原生組件(如video)

· WebView 建立一個 <embed> 節點並指定其類型爲video

· chromium 內核建立一個 WebPlugin 實例,並生成一個 RenderLayer

· 原生層將原生組件的畫面繪製到 RenderLayer 所綁定的 SurfaceTexture 上

· chromium 渲染該 RenderLayer

這個流程至關於給 WebView 添加了一個外置插件,且<embed>節點是真正的 DOM 節點,可將更多的樣式做用於該節點上。

將來可能

若是要探討小程序接下來的技術升級方向,咱們認爲應該在用戶體驗、開發效率兩個方向上努力。

更優秀的用戶體驗

先說用戶體驗的問題,主要也是兩個方面:

· 解決現有的性能坑點,好比前面分析的這幾項,通信阻塞、分層限制等,這裏再也不贅述

· 支持更多App的體驗,更自由靈活的配置,好比高斯模糊

若是你也想快速搭建的本身的小程序引擎,並更優的解決如上體驗問題,該怎麼辦?

這裏放一個福利。

uni-app發行到App端,其實是一個功能更豐富的小程序引擎,DCloud會在近期將這個小程序SDK完整開源,歡迎你們基於uni-app小程序SDK快速打造本身的小程序平臺。

uni-app小程序SDK具有以下幾個特徵:

· 更接近App的性能:支持純native view和webview雙引擎渲染,擴展的wxs,更高的通信性能

· 更接近App的功能:提供豐富的原生API

· 開放性更強:更靈活的配置,支持更多小程序難以實現的豐富交互效果

· 開源不受限:無需簽定任何協議,拿走就用

· 生態豐富:支持微信小程序自定義組件,支持全部uni-app插件,uni-app插件市場目前已有上千款成熟插件

OK,個人分享到此結束,如有錯誤,歡迎交流指正。

相關文章
相關標籤/搜索