隨著各大平臺小程序的快速放量,開發(fā)者遇到越來越多的平臺適配問題。各平臺小程序的性能優(yōu)化方法也各不相同,我們該如何應(yīng)對?DCloud CTO 崔紅保在 GMTC 深圳 2019(全球大前端技術(shù)大會)分享了《小程序的未來方向》,介紹了小程序技術(shù)架構(gòu)、性能卡點(diǎn)以及各平臺優(yōu)化方案,對于小程序未來的技術(shù)更迭,提出了小程序在未來可能的發(fā)展方向。本文根據(jù)演講內(nèi)容整理而成。
簡單介紹一下我自己,中年碼農(nóng),跨平臺開發(fā)領(lǐng)域的老兵。在那個(gè)翻蓋摩托羅拉手機(jī)代表著先進(jìn)和時(shí)髦的年代,我就開始參與 “window mobile/j2me/symbain” 等系統(tǒng)的跨平臺研發(fā)管理工作,可能很多同學(xué)都沒見過那些手機(jī)。到后來的移動互聯(lián)網(wǎng)時(shí)代及當(dāng)下的小程序時(shí)代,我也一直在深度參與其中,持續(xù)輸出“Hybrid App”引擎、前端 UI 庫(mui)及小程序跨端開發(fā)框架(uni-app)。目前在 DCloud 任職 CTO,同時(shí)兼 “uni-app” 產(chǎn)品負(fù)責(zé)人。
羅馬不是一天建成的,小程序也不是一天發(fā)明的。小程序這種介于 H5 和 Native App 之間的特殊應(yīng)用形態(tài),從探索到成熟,經(jīng)歷了哪些過程?我們首先帶大家回顧梳理一下。然后,從現(xiàn)有技術(shù)架構(gòu)出發(fā),分析小程序當(dāng)下幾個(gè)主要性能坑點(diǎn)。各家小程序引擎為解決這些坑點(diǎn),做了哪些完善工作。比如,大家知道小程序是以 Web 渲染為主、原生渲染為輔,那引入原生渲染后,引發(fā)了哪些新的問題?為解決這些問題,微信提出了同層渲染的方案,同層渲染在技術(shù)層面上又是如何實(shí)現(xiàn)的?最后從當(dāng)前已知問題出發(fā),對于小程序未來的技術(shù)更迭,拋出一些我們認(rèn)為的可能方向,供大家參考。
一、小程序歷史
HTML5 于 2007 年在 W3C 立項(xiàng),與 iPhone 發(fā)布同年。
喬布斯曾期待 HTML5 能幫助 iPhone 打造起應(yīng)用生態(tài)系統(tǒng)。但 HTML5 的發(fā)展速度并不如預(yù)期,雖然它成功地打破了 IE Flash 壟斷的局面,卻沒有達(dá)到承載優(yōu)秀的移動互聯(lián)網(wǎng)體驗(yàn)的地步。
蘋果公司在 iPhone 站穩(wěn)腳跟后,緊接著發(fā)布了自己的 App Store,開啟了移動互聯(lián)網(wǎng)的原生應(yīng)用時(shí)代。
大家知道現(xiàn)在手機(jī)端主要是 iOS、Android 兩大系統(tǒng),實(shí)際上在早期有 3 大系統(tǒng)競爭,還有一個(gè)就是諾基亞的 MeeGo 系統(tǒng),MeeGo 采用 C HTML5 的雙模應(yīng)用生態(tài)策略。然而,C 的開發(fā)難度太大,HTML5 體驗(yàn)又不行,所以后來 MeeGo 就掉隊(duì)了;與之對應(yīng),Android 依靠 Java 技術(shù)生態(tài),在競爭中脫穎而出。
于是在移動互聯(lián)網(wǎng)初期,應(yīng)用生態(tài)被定了基調(diào) —— 原生開發(fā)。
國內(nèi)有一批做瀏覽器的廠商,嘗試去改進(jìn) HTML5。比如,百度在 2013 年的百度世界大會上發(fā)布了輕應(yīng)用,通過給 WebView 擴(kuò)展原生能力,補(bǔ)充 JS API,讓 HTML5 應(yīng)用可以實(shí)現(xiàn)更多功能。
這類業(yè)務(wù)發(fā)展的頂峰,是微信在 2015 年初發(fā)布的微信 JS SDK,作為國內(nèi)事實(shí)上最大的手機(jī)瀏覽器,微信為它的瀏覽器內(nèi)核擴(kuò)充了大量 JS API,讓開發(fā)者可以用 JS 調(diào)用微信支付、掃碼等眾多 HTML5 做不到的功能。
不過這類業(yè)務(wù)沒有取得成功,HTML5 的問題不止是功能不足,性能體驗(yàn)是更嚴(yán)重的問題。而體驗(yàn)問題,不是簡單地?cái)U(kuò)展 JS 能力能搞定的。
與瀏覽器不同,Hybrid 應(yīng)用是另一個(gè)細(xì)分領(lǐng)域,開發(fā)者使用 JS 編寫應(yīng)用,為了讓 JS 應(yīng)用更接近原生應(yīng)用的功能體驗(yàn),這個(gè)行業(yè)的從業(yè)者做出了很多嘗試。我們 DCloud 公司是業(yè)內(nèi)主流 Hybrid App 引擎提供方之一,我們提出了改進(jìn) HTML5 的“性能功能”障礙的解決方案 —— 通過工具、引擎優(yōu)化、開發(fā)模式調(diào)整,讓開發(fā)者可以通過 JS 寫出更接近原生 App 體驗(yàn)的應(yīng)用。
多 WebView 模式,原生接管轉(zhuǎn)場動畫、下拉刷新、Tab 分頁,預(yù)載 WebView……各種優(yōu)化技術(shù)不停迭代,終于讓 Hybrid 應(yīng)用取得了性能體驗(yàn)的突破。
Hybrid 應(yīng)用和輕應(yīng)用、微信 JS SDK 等基于瀏覽器增加方案相比,還有一個(gè)巨大的差別:一個(gè)是 Client/Server,一個(gè)是 Browser/Server。簡單來說,Hybrid 應(yīng)用是 JS 編寫的需要安裝的 App,而輕應(yīng)用是在線網(wǎng)頁。
C/S 的應(yīng)用在每次頁面加載時(shí),僅需要聯(lián)網(wǎng)獲取 JSON 數(shù)據(jù);而 B/S 應(yīng)用除了 JSON 數(shù)據(jù)外,還需要每次從服務(wù)器加載頁面 DOM、樣式、邏輯代碼,所以 B/S 應(yīng)用的頁面加載很慢,體驗(yàn)很差。
可是這樣的 C/S 應(yīng)用雖然體驗(yàn)好,卻失去了 HTML5 的動態(tài)性,仍然需要安裝、更新,無法即點(diǎn)即用、直達(dá)二級頁面。
那么 C/S 應(yīng)用的動態(tài)性是否可以解決呢?對此, DCloud 率先提出了“流應(yīng)用”概念,把之前 Hybrid 應(yīng)用里的運(yùn)行于客戶端的 JS 代碼,先打包發(fā)布到服務(wù)器,制定流式加載協(xié)議,手機(jī)端引擎動態(tài)下載這些 JS 代碼到本地,并且為了第一次加載速度更快,實(shí)現(xiàn)了應(yīng)用的邊下載邊運(yùn)行。
就像流媒體的邊下邊播一樣,應(yīng)用也可以實(shí)現(xiàn)邊用邊下。
在這套方案的保障下,終于解決了之前的各種難題:讓 JS 應(yīng)用功能體驗(yàn)達(dá)到原生,并且可即點(diǎn)即用、直達(dá)二級頁面。
接著就是微信小程序,最初的名字實(shí)際上是微信應(yīng)用號,之后改名為小程序,2016 年 9 月份內(nèi)測,2017 年 1 月正式發(fā)行,再之后阿里巴巴、手機(jī)廠商聯(lián)盟、百度、今日頭條,陸續(xù)推出了自己的小程序平臺,小程序時(shí)代滾滾而來。
2018 年 9 月,微信推出云開發(fā),這個(gè)功能我們認(rèn)為是小程序發(fā)展歷史上的一個(gè)重要節(jié)點(diǎn),它可以讓前端工程師從前到后將所有業(yè)務(wù)閉環(huán)實(shí)現(xiàn),減少前后端的溝通成本、人力成本、運(yùn)維成本,屬于開發(fā)模式的重大升級。與之前的前端同學(xué)既可通過 JS/CSS 編寫前端 UI,又可通過 “Node.js” 寫后端業(yè)務(wù),這種所謂全棧開發(fā)模式相比,云開發(fā)有更好的優(yōu)勢,因?yàn)榍岸送瑢W(xué)對于 DB 優(yōu)化、彈性擴(kuò)容、攻擊防護(hù)、災(zāi)備處理等方面還是有經(jīng)驗(yàn)欠缺的,但云開發(fā)將這些都封裝好了,真正做到僅專注業(yè)務(wù)實(shí)現(xiàn),其它都委托云廠商服務(wù)。
二、小程序架構(gòu)
這是一個(gè)比較通用的小程序架構(gòu),目前幾家小程序架構(gòu)設(shè)計(jì)大致都是這樣的(快應(yīng)用的區(qū)別是視圖層只有原生渲染)。
大家知道小程序是一個(gè)邏輯、視圖層分離的架構(gòu)。
邏輯層就是上圖左上角這塊,小程序中開發(fā)的所有頁面 JS 代碼,最后都會打包合并到邏輯層,邏輯層除了執(zhí)行開發(fā)者的業(yè)務(wù) JS 代碼外,還需處理小程序框架的內(nèi)置邏輯,比如 App 生命周期管理。
視圖層就是上圖右上角這塊,用戶可見的 UI 效果、可觸發(fā)的交互事件在視圖層完成,視圖層包含 Web 組件、原生組件兩種,也就是小程序是原生 Web 混合渲染的模式,這塊后面會詳細(xì)講。
邏輯層最后運(yùn)行在 JS CORE 或 V8 環(huán)境中;JS CORE 既不是 DOM 環(huán)境,也不是 Node 環(huán)境,你是無法使用 JS 中的 DOM 或 BOM 對象的,你能調(diào)用的僅僅是 ECMAScript 標(biāo)準(zhǔn)規(guī)范中所給出的方法。
那如果你要發(fā)送網(wǎng)絡(luò)請求怎么辦?window.XMLHttpRequest 是無法使用的(當(dāng)然即使可以調(diào)用,在 iOS 的 WKWebView 中也存在更嚴(yán)格的跨域限制,會有問題)。這時(shí)候,網(wǎng)絡(luò)請求就需要通過原生的網(wǎng)絡(luò)模塊來發(fā)送,JS CORE 和原生之間呢,就需要這個(gè) JS Bridge 來通訊。
三、架構(gòu)引發(fā)的性能坑點(diǎn)
小程序這種架構(gòu),最大的好處是新頁面加載可以并行,讓頁面加載更快,且不卡轉(zhuǎn)場動畫;但同時(shí)也引發(fā)了部分性能坑點(diǎn),今天主要介紹 3 點(diǎn):
1. 邏輯層 / 視圖層通訊阻塞
我們從“swipeaction”這個(gè)例子講起,需求是用戶在列表項(xiàng)上向左滑動,右側(cè)隱藏的菜單跟隨用戶手勢平滑移動。
若想在小程序架構(gòu)上實(shí)現(xiàn)流暢的跟手滑動,是很困難的,為什么?
回顧一下小程序架構(gòu),小程序的運(yùn)行環(huán)境分為邏輯層和視圖層,分別由 2 個(gè)線程管理,小程序在視圖層與邏輯層兩個(gè)線程間提供了數(shù)據(jù)傳輸和事件系統(tǒng)。這樣的分離設(shè)計(jì),帶來了顯而易見的好處:
環(huán)境隔離,既保證了安全性,同時(shí)也是一種性能提升的手段,邏輯和視圖分離,即使業(yè)務(wù)邏輯計(jì)算非常繁忙,也不會阻塞渲染和用戶在視圖層上的交互。
但同時(shí)也帶來了明顯的壞處:
視圖層(WebView)中不能運(yùn)行 JS,而邏輯層 JS 又無法直接修改頁面 DOM,數(shù)據(jù)更新及事件系統(tǒng)只能靠線程間通訊,但跨線程通信的成本極高,特別是需要頻繁通信的場景。
基于這樣的架構(gòu)設(shè)計(jì),我們回到“swipeaction”,分析一次 touchmove 的操作,小程序內(nèi)部的響應(yīng)過程:
(1)用戶拖動列表項(xiàng),視圖層觸發(fā) touchmove 事件,經(jīng) Native 層中轉(zhuǎn)通知邏輯層(邏輯層、視圖層不是直接通訊的,需 Native 中轉(zhuǎn)),即下圖中的?、?兩步;
(2)邏輯層計(jì)算需移動的位置,然后再通過 setData 傳遞位置數(shù)據(jù)到視圖層,中間同樣會由微信客戶端(Native)做中轉(zhuǎn),即下圖中的?、?兩步。
實(shí)際上,用戶滑動過程中,touchmove 的回調(diào)觸發(fā)是非常頻繁的,每次回調(diào)都需要 4 個(gè)步驟的通訊過程,高頻率回調(diào)導(dǎo)致通訊成本大幅增加,極有可能導(dǎo)致頁面卡頓或抖動。為什么會卡頓,因?yàn)橥ㄓ嵦^頻繁,視圖層無法在 16ms 內(nèi)完成 UI 更新。
為解決這種通訊阻塞的問題,各家小程序都在逐步提供對應(yīng)的解決方案,比如微信的 WXS、支付寶的 SJS、百度的 Filter,但每家小程序支持情況不同,詳細(xì)見下表。
另外,微信的“關(guān)鍵幀動畫”、百度的“animation-view” Lottie 動畫,也是為減少頻繁通訊的一種變更方式。
其實(shí),通訊阻塞是業(yè)界普遍存在的一個(gè)問題,不止小程序,“React Native”“Weex”等同樣存在通訊阻塞的問題。只不過“React Native”“Weex”的視圖層是原生渲染,而小程序是 Web 渲染。我們下面以“Weex”為例來說明。
大家知道,“Weex”底層使用的 JS-Native Bridge,這個(gè) Bridge 使得 JS 和 Native 之間的通信會有固定的性能損耗。
繼續(xù)以上述“swipeaction”為例,要實(shí)現(xiàn)列表項(xiàng)菜單的跟手滑動,大致需經(jīng)如下流程:
(1)在 UI 視圖上綁定 touch 事件(或 pan 事件);
(2)當(dāng)手勢觸發(fā)時(shí), Native UI 層將手勢事件通過 Bridge 傳遞給 JS 邏輯層 , 這產(chǎn)生了一次 Native UI 到 JS 邏輯的通信,即下圖中的?、?兩步 ;
(3)JS 邏輯在接收到事件后,根據(jù)手指移動的偏移量驅(qū)動界面變化,這又會產(chǎn)生一次 JS 到 Native UI 的通信,即下圖中的?、?兩步。
同樣,手勢回調(diào)事件觸發(fā)的頻率是非常高的,頻繁的的通信帶來的時(shí)間成本很可能導(dǎo)致界面無法在 16ms 中完成繪制,卡頓也就產(chǎn)生了。
“Weex”為解決通訊阻塞,提供了“BindingX”解決方案,這是一種稱之為“Expression Binding”的機(jī)制,簡要介紹一下:
(1)接收手勢事件的視圖,在移動過程中的偏移量以“x,y”兩個(gè)變量表示;
(2)期望改變(跟隨移動)的視圖,變化的屬性為“translateX”和“translateY”,對應(yīng)變化的偏移量以“f(x),f(y)”表達(dá)式表示;
(3)將”交互行為 " 以表達(dá)式的方式描述,并提前預(yù)置到 Native UI 層;
(4)交互觸發(fā)時(shí),Native UI 根據(jù)其內(nèi)置的表達(dá)式解析引擎,去執(zhí)行表達(dá)式,并根據(jù)表達(dá)式執(zhí)行的結(jié)果驅(qū)動視圖變換,這個(gè)過程無需和 JS 邏輯通訊。
偽代碼 – 摘錄自 Weex 官網(wǎng)
復(fù)制代碼
{ anchor: foo_view.ref // ----> 這是 " 產(chǎn)生手勢的視圖 " 的引用 props: [ { element: foo_view.ref, // ----> 這是 " 期望改變的視圖 " 的引用 expression: f(x) = x, // ----> 這是具體的表達(dá)式 property: translateX // ----> 這是期望改變的屬性 }, { element: foo_view.ref, expression: f(y) = y, // ----> y 屬性 property: translateY } ]}
“React Native”同樣存在類似問題,為避免頻繁的通信,“React Native”生態(tài)也有對應(yīng)方案,比如“Animated”組件及 Lottie 動畫支持。以 “Animated”組件為例,為實(shí)現(xiàn)流暢的動畫效果,該組件采用了聲明式的 API,在 JS 端僅定義了輸入與輸出以及具體的 transform 行為,而真正的動畫是通過 Native Driver 在 Native 層執(zhí)行,這樣就避免了頻繁的通信。然而,聲明式的方式能夠定義的行為有限,無法勝任交互場景。
“uni-app”在 App 端同樣面臨通訊阻塞的問題,我們目前的方案是采用類似微信 WXS 的機(jī)制(內(nèi)部叫“renderjs”),但放開了 WXS 中無法獲取頁面 DOM 元素的限制,比如下圖中多個(gè)小球同時(shí)移動的 canvas 動畫,“uni-app”在 App 端的實(shí)現(xiàn)方案是:
(1)renderjs 中獲取 canvas 對象;
(2)基于 web 的 canvas 繪制動畫,而非原生 canvas 繪制。
Tips:大家需要注意,并不是所有場景都是原生性能更好,小程序架構(gòu)下,如上多球同時(shí)移動的動畫,原生 canvas 并不如在 WXS(renderjs)中直接調(diào)用 Web canvas
下表總結(jié)了跨端框架在通訊阻塞方面的解決方案:
2. 數(shù)據(jù) / 組件差量更新
小程序架構(gòu)存在通訊阻塞問題,廠商為解決這個(gè)問題,創(chuàng)造了“WXS”腳本語言及關(guān)鍵幀動畫等方式,但這些都是廠商維度的優(yōu)化方案。我們作為小程序開發(fā)者,在性能優(yōu)化方面,又能做哪些工作呢?
小程序開發(fā)性能優(yōu)化,核心就是“setData”的調(diào)用,你能做只有兩件事情:
- 盡量少調(diào)用“setData”;
- 每次調(diào)用“setData”,傳遞盡可能少的數(shù)據(jù)量,即數(shù)據(jù)差量更新。
(1)減少 setData 調(diào)用次數(shù)
假設(shè)我們有更改多個(gè)變量值的需求,示例如下:
change:function(){? ? this.setData({a:1});? ? ... // 其它業(yè)務(wù)邏輯? ? this.setData({b:2});? ? ... // 其它業(yè)務(wù)邏輯? ? this.setData({c:3});? ? ... // 其它業(yè)務(wù)邏輯? ? this.setData({d:4});}
如上,4 次調(diào)用“setData”,會引發(fā) 4 次邏輯層、視圖層數(shù)據(jù)通訊。這種場景,開發(fā)者需意識到“setData”有極高的調(diào)用代價(jià),自己需手動調(diào)整代碼,合并數(shù)據(jù),減少數(shù)據(jù)通訊次數(shù)。
部分小程序三方框架已內(nèi)置數(shù)據(jù)合并的能力,比如“uni-app”在 Vue runtime 上進(jìn)行了深度定制,開發(fā)者無需關(guān)注“setData”的調(diào)用代價(jià),可放心編寫如下代碼:
change:function(){? ? this.a = 1;? ? ... // 其它業(yè)務(wù)邏輯? ? this.b = 2;? ? ... // 其它業(yè)務(wù)邏輯? ? this.c = 3;? ? ... // 其它業(yè)務(wù)邏輯? ? this.d = 4;}
如上 4 次賦值,uni-app 運(yùn)行時(shí)會自動合并成“{“a”:1,“b”:2,“c”:3,“d”:4}”一條記錄,調(diào)用一次“setData”完成所有數(shù)據(jù)傳遞,大幅降低 setData 的調(diào)用頻次,結(jié)果如下圖:
減少“setData”調(diào)用次數(shù),還有個(gè)注意點(diǎn):后臺頁面(用戶不可見的頁面)應(yīng)避免調(diào)用“setData”。
(2)數(shù)據(jù)差量更新
假設(shè)我們有一個(gè) “列表頁 上拉加載” 的場景,初始化列表項(xiàng)為 “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); // 列表項(xiàng)新增記錄? ? ? ? this.setData({? ? ? ? ? ? list:this.data.list? ? ? ? })? ? }})
如上代碼,change 方法執(zhí)行時(shí),會將 list 中的 “item1 ~ item8”8 個(gè)列表項(xiàng)通過“setData”全部傳輸過去,而實(shí)際上變化的數(shù)據(jù)只有“item5 ~ item8”。
開發(fā)者在這種場景下,應(yīng)通過差量計(jì)算,僅通過“setData”傳遞變化的數(shù)據(jù),如下是一個(gè)示例代碼:
page({? ? data:{? ? ? ? list:['item1','item2','item3','item4']? ? },? ? change:function(){? ? ? ? // 通過長度獲取下一次渲染的索引? ? ? ? let index = this.data.list.length;? ? ? ? let newData = ['item5','item6','item7','item8'];? ? ? ? let newDataObj = {};// 變化的數(shù)據(jù)? ? ? ? newData.forEach((item) => {? ? ? ? ? ? newDataObj['list[' (index ) ']'] = item;// 通過 list 下標(biāo)精確控制變更內(nèi)容? ? ? ? });? ? ? ? this.setData(newDataObj) // 設(shè)置差量數(shù)據(jù)? ? }})
每次都手動計(jì)算差量變更數(shù)據(jù)是繁瑣的,新手不理解小程序原理的話,也容易忽略這些性能點(diǎn),給 App 埋下性能坑點(diǎn)。
此處,建議開發(fā)者選擇成熟的第三方小程序框架,這些框架已經(jīng)自動封裝差量數(shù)據(jù)計(jì)算,對開發(fā)者更友好。比如,“uni-app”借鑒了 “westore JSON Diff”庫,在調(diào)用 setData 之前,會先比對歷史數(shù)據(jù),精確高效計(jì)算出有變化的差量數(shù)據(jù),然后再調(diào)用 setData,僅傳輸變化的數(shù)據(jù),這樣可實(shí)現(xiàn)傳遞數(shù)據(jù)量的最小化,提升通訊性能。如下,是一個(gè)示例代碼:
export default{? ? data(){? ? ? ? return {? ? ? ? ? ? list:['item1','item2','item3','item4']? ? ? ? }? ? },? ? methods:{? ? ? ? change:function(){? ? ? ? ? ? let newData = ['item5','item6','item7','item8'];? ? ? ? ? ? this.list.push(...newData) // 直接賦值,框架會自動計(jì)算差量數(shù)據(jù)? ? ? ? }? ? }}
Tips:如上 change 方法執(zhí)行時(shí),僅會將 list 中的 “item5 ~ item8”4 個(gè)新增列表項(xiàng)傳輸過去,實(shí)現(xiàn)了 setData 傳輸量的極簡化。
(3)組件差量更新
下圖是一個(gè)微博列表截圖:
假設(shè)當(dāng)前有 200 條微博,用戶對某條微博點(diǎn)贊,需實(shí)時(shí)變更其點(diǎn)贊數(shù)據(jù)(狀態(tài));在傳統(tǒng)模式下,一條微博的點(diǎn)贊狀態(tài)變更,會將整個(gè)頁面 (Page) 的數(shù)據(jù)全部通過 setData 傳遞過去,這個(gè)消耗是非常高的;而即使通過之前介紹,通過差量計(jì)算的方式獲取變更數(shù)據(jù),這個(gè) Diff 遍歷范圍也很大,計(jì)算效率極低。
如何實(shí)現(xiàn)更高性能的微博點(diǎn)贊?這其實(shí)就是組件更新的典型場景。
合適的方式應(yīng)該是,將每條微博封裝成一個(gè)組件,用戶點(diǎn)贊后,僅在當(dāng)前組件范圍內(nèi)計(jì)算差量數(shù)據(jù)(可理解為 Diff 范圍縮小為原來的 1/200),這樣效率才是最高的。
提醒大家注意,并不是所有小程序三方框架都已實(shí)現(xiàn)自定義組件,只有在基于自定義組件模式封裝的框架中,性能才會大幅提升;如果三方框架是基于老的“template”模板封裝的組件開發(fā),則性能并不會有明顯改善,其 Diff 對比范圍依然是 Page 頁面級的。
3. 混合渲染
大家知道,小程序當(dāng)中有一類特殊的內(nèi)置組件——原生組件,這類組件有別于 WebView 渲染的內(nèi)置組件,他們是由原生客戶端渲染的。
小程序中的原生組件,從使用方式上來說,主要分為三類:
- 通過配置項(xiàng)創(chuàng)建的:選項(xiàng)卡、導(dǎo)航欄,還有下拉刷新;
- 通過組件名稱創(chuàng)建的,比如:camera、canvas、input、live-player、live-pusher、map、textarea、video;
- 通過 API 接口創(chuàng)建的,比如:showModal、showActionSheet 等。
除了上面提到的這些之外,其它基本都是 Web 渲染。所以說,小程序是混合渲染模式,Web 渲染為主,原生渲染為輔。
(1)為什么要引入混合渲染
接下來的問題,為什么要引入原生渲染?以及為什么僅針對這幾個(gè)組件提供了原生增強(qiáng)?其他組件為什么沒有做原生實(shí)現(xiàn)?
這就需要我們針對每個(gè)組件單獨(dú)進(jìn)行分析思考,這里舉了幾個(gè)例子:
- tabs/navigationbar:避免切換頁面白屏,提升新窗口進(jìn)入時(shí)的用戶體驗(yàn)。雖然不使用原生的 tabbar 和導(dǎo)航欄,可以做出更靈活的界面,但在切換頁面那短短 300ms 內(nèi),想保證頁面不白屏,還是需要使用渲染更快的原生 tabbar 和導(dǎo)航欄;
- video:全屏后的滑動控制(聲音、進(jìn)度、亮度等);
- map:更流暢的雙指縮放、位置拖動;
- input:Web 端的 input,鍵盤彈出時(shí),只有“完成”按鈕,無法讓鍵盤顯示“發(fā)送”“下一個(gè)”這樣的按鍵。
提到“input”控件的原生化,可以稍微發(fā)散一下。
小程序中原生 input 控件的通用做法是,未獲取焦點(diǎn)時(shí)以 Web 控件顯示,但在獲取焦點(diǎn)時(shí),繪制一個(gè)原生 input,蓋在 Web input 上方,此時(shí),用戶看見的鍵盤即為原生 input 所對應(yīng)的鍵盤,原生彈出鍵盤是可自定義按鈕(如上圖中下一步、send 按鈕)。這種做法存在一個(gè)缺陷: Web 和原生,畢竟不同渲染引擎,在鍵盤彈出和關(guān)閉時(shí),對應(yīng) input 的“placeholder”會閃爍。
在 Android 平臺,還有一種做法是基于 WebKit 改造,定制彈出鍵盤樣式;這種方案,在鍵盤彈出和關(guān)閉時(shí),input 控件都是 Web 實(shí)現(xiàn)的,故不存在“placeholder”閃爍的問題。
(2)混合渲染引發(fā)的問題
原生組件雖然帶來了更豐富的特性及更好的性能,但同時(shí)也引入了一些新的問題,比如:
- 層級問題:原生永遠(yuǎn)在最高層,無法通過“z-index”設(shè)置不同元素的層級,無法與 view、image 等內(nèi)置組件相互覆蓋,不支持在“picker-view”“scroll-view”“swiper”等組件中使用;
- 通訊問題:比如一個(gè)長列表中內(nèi)嵌視頻組件,頁面滾動時(shí),需通知原生的視頻組件一起滾動,通訊阻塞,可能導(dǎo)致組件抖動或拖影;
- 字體問題:在 Android 手機(jī)上,調(diào)整系統(tǒng)主題字體,所有原生渲染的控件的字體都會變化,而 Web 渲染的字體則不會變化。如下圖,系統(tǒng) rom 字體為一款“你的名字”的三方字體,設(shè)置后,小程序頂部標(biāo)題字體變了,底部選項(xiàng)卡字體也變了,但小程序中間內(nèi)容區(qū)字體不變,這就是比較尷尬的一種情況,一個(gè)頁面,兩種字體。
當(dāng)然,并不是所有小程序都存在這種問題,部分小程序通過修改自帶的 WebView 內(nèi)核,實(shí)現(xiàn)了 WebView 也可以使用 rom 主題字體,比如微信、QQ、支付寶;其他小程序(百度、頭條),WebView 仍然無法渲染為 rom 主題字體。
(3) 混合渲染改進(jìn)方案
既然混合渲染有這些問題,對應(yīng)就會有解決方案,目前已有的方案如下。
- 方案?:創(chuàng)造層級更高的組件
既然其它組件無法覆蓋到原生組件上,那就創(chuàng)造出一種新的組件,讓這個(gè)新組件可以覆蓋到 video 或 map 上?!癱over-view/cover-image”就是基于這種需求創(chuàng)造出來的新組件;其實(shí)它們也是原生組件,只不過層級略高,可以覆蓋在 map、video、canvas、camera 等原生組件上。
目前除了字節(jié)跳動外,其它幾家小程序均已支持“cover-view/cover-image”。
cover-view/cover-image 在一定程度上緩解了分層覆蓋的問題,但也有部分限制,比如嚴(yán)格的嵌套順序。
- 方案?:消除分層,同層渲染
既然分層有問題,那就消除分層,從 2 層變成 1 層,所有組件都在一個(gè)層中,“z-index”豈不就可生效了?
這個(gè)小目標(biāo)說起來簡單,具體實(shí)現(xiàn)還是很復(fù)雜的。
4. 同層渲染
拋開小程序當(dāng)前架構(gòu)實(shí)現(xiàn),解決混合渲染最直接的方案,應(yīng)該更換渲染引擎,全部基于原生渲染,video/map 和 image/view 均為原生控件,層級相同,層級遮蓋問題自然消失。這正是“uni-app”在 App 端的推薦方案。
當(dāng)前 Web 渲染為主、原生渲染為輔的主流小程序現(xiàn)狀,如何實(shí)現(xiàn)同層渲染?
基于我們的分析研究,這里簡單講解一下同層渲染實(shí)現(xiàn)的方案,和微信真實(shí)實(shí)現(xiàn)可能會有出入(目前僅微信一家實(shí)現(xiàn)了同層渲染)。
(1) iOS 平臺
小程序在 iOS 端使用 WKWebView 進(jìn)行渲染,WKWebView 在內(nèi)部采用的是分層渲染,一般會將多個(gè) DOM 節(jié)點(diǎn),合并到一個(gè)層上進(jìn)行渲染。因此,DOM 節(jié)點(diǎn)和層之間不存在一一對應(yīng)關(guān)系。但是,一旦將一個(gè) DOM 節(jié)點(diǎn)的 CSS 屬性設(shè)置為 “overflow: scroll” 后,WKWebView 便會為其生成一個(gè) WKChildScrollView,且 WebKit 內(nèi)核已經(jīng)處理了 WKChildScrollView 與其他 DOM 節(jié)點(diǎn)之間的層級關(guān)系,這時(shí) DOM 節(jié)點(diǎn)就和層之間有一一對應(yīng)關(guān)系了。
小程序 iOS 端的同層渲染可基于 WKChildScrollView 實(shí)現(xiàn),主要流程如下:
- 創(chuàng)建一個(gè) DOM 節(jié)點(diǎn)并設(shè)置其 CSS 屬性為 overflow: scroll;
- 通知原生層查找到該 DOM 節(jié)點(diǎn)對應(yīng)的原生 WKChildScrollView 組件;
- 將原生組件掛載到該 WKChildScrollView 節(jié)點(diǎn)上作為其子 View。
(2)Android 平臺
小程序在 Android 端采用 Chromium 作為 WebView 渲染層,和 iOS 的 WKWebView 不同,是統(tǒng)一渲染的,不會分層渲染。但 Chromium 支持 WebPlugin 機(jī)制,WebPlugin 是瀏覽器內(nèi)核的一個(gè)插件機(jī)制,可用來解析“< embed >”。Android 端的同層渲染可基于 “< embed >”加 Chromium 內(nèi)核擴(kuò)展來實(shí)現(xiàn),大致流程如下:
- 原生層創(chuàng)建一個(gè)原生組件(如 video);
- WebView 創(chuàng)建一個(gè) “< embed >”節(jié)點(diǎn)并指定其類型為 video;
- Chromium 內(nèi)核創(chuàng)建一個(gè) WebPlugin 實(shí)例 * 并生成一個(gè) RenderLayer;
- 原生層將原生組件的畫面繪制到 RenderLayer 所綁定的 SurfaceTexture 上;
- Chromium 渲染該 RenderLayer。
這個(gè)流程相當(dāng)于給 WebView 添加了一個(gè)外置插件,且“< embed >”節(jié)點(diǎn)是真正的 DOM 節(jié)點(diǎn),可將更多的樣式作用于該節(jié)點(diǎn)上。
四、未來可能
如果要探討小程序接下來的技術(shù)升級方向,我們認(rèn)為應(yīng)該在用戶體驗(yàn)、開發(fā)效率兩個(gè)方向上努力。
1. 更優(yōu)秀的用戶體驗(yàn)
先說用戶體驗(yàn)的問題,主要也是兩個(gè)方面:
- 解決現(xiàn)有的性能坑點(diǎn),比如前面分析的這幾項(xiàng),通訊阻塞、分層限制等,這里不再贅述;
- 支持更多 App 的體驗(yàn),更自由靈活的配置,比如,高斯模糊。
如果你也想快速搭建的自己的小程序引擎,并更優(yōu)的解決如上體驗(yàn)問題,該怎么辦?
uni-app 發(fā)行到 App 端,實(shí)際上就是一個(gè)完整的小程序引擎,DCloud 會在近期將這個(gè)引擎完整開源,歡迎大家基于 uni-app 小程序 SDK 快速打造自己的小程序平臺。
uni-app 小程序 SDK 具備如下幾個(gè)特征:
- 性能:支持 Native 渲染,擴(kuò)展 WXS,更高的通訊性能;
- 開放性:更靈活的配置,支持更多 App 的體驗(yàn);
- 開源不受限:無需簽訂任何協(xié)議,拿走就用;
- 生態(tài)豐富:支持微信小程序自定義組件,支持所有“uni-app”插件,且其插件市場目前已有上千款成熟插件。
2. 開發(fā)效率
開發(fā)效率應(yīng)該從跨端、跨云兩個(gè)維度進(jìn)行分析。
(1)跨端開發(fā)
目前的小程序都帶有明顯的廠家屬性,每個(gè)廠家各不相同。比如,阿里內(nèi)部有多套小程序(支付寶、淘寶、釘釘?shù)龋?,幸好阿里?nèi)部目前已基本統(tǒng)一。但騰訊體系下,微信和 QQ 小程序依然是兩隊(duì)人馬,兩套規(guī)范。
小程序之前是手機(jī)端的,2019 年 360 出了 PC 端小程序。
接下來,會不會還有其它廠家推出自己的小程序?會不會有新的端的出現(xiàn)?比如,面向電視的小程序、面向車載的小程序?
一切皆有可能。
逐水草而居是人類的本能,追求流量依然是互聯(lián)網(wǎng)的制勝法寶。當(dāng)前的小程序宿主,都是億級流量入口,且各家流量政策不同。比如,微信的流量雖然很大,但有各種限制;百度和頭條是支持廣告投放的,通過廣告投放,可以快速獲得大量較為精準(zhǔn)的用戶;百度小程序還有個(gè) Web 化的功能,可以將 Web 的搜索流量,轉(zhuǎn)化成小程序的流量。
面對眾多小程序平臺及各自巨大的入口流量,開發(fā)者如何應(yīng)對?
等待 W3C 的小程序標(biāo)準(zhǔn)統(tǒng)一,短期不太現(xiàn)實(shí)。當(dāng)下,若想將業(yè)務(wù)快速觸達(dá)多家小程序,借助跨端框架應(yīng)該是唯一可行的方案。
(2)跨云開發(fā)
開發(fā)商借助“uni-app”或其它跨端框架,雖然已可以開發(fā)所有前端應(yīng)用。但仍然需要雇傭 PHP 或 Java 等后臺開發(fā)人員,既有后端人員成本,又有前 / 后端溝通成本。
騰訊、阿里、百度小程序雖陸續(xù)上線了云開發(fā),但它們均只支持自己的小程序,無法跨端,分散的服務(wù)器對開發(fā)商更不可取。
故我們認(rèn)為跨廠商的 Serverless 是接下來的一個(gè)重點(diǎn)需求,開發(fā)者在一個(gè)云端存儲所有業(yè)務(wù)數(shù)據(jù)及后端邏輯,然后將前端小程序發(fā)行到各家小程序平臺,也就是“一云多端”模式。
五、小結(jié)
基于小程序的現(xiàn)狀,我們也許可以總結(jié)一下小程序技術(shù)上的可能方向:
- 其它小程序拉齊與微信的差距,讓開發(fā)者可以做出足夠高性能的應(yīng)用服務(wù);
- 所有小程序應(yīng)拉齊和 App 的體驗(yàn)差距,雖然功能 API 方面仍有不足,但操作性能和交互體驗(yàn),不應(yīng)該弱于 App;
- 跨端框架 Serverless,讓開發(fā)者更輕松,讓企業(yè)更高效。
作者介紹:
崔紅保,DCloud CTO,Uni-App 團(tuán)隊(duì)負(fù)責(zé)人,開發(fā)了 2 個(gè) Github Star 上萬的流行項(xiàng)目。有 10 年以上研發(fā)管理經(jīng)驗(yàn),在跨平臺引擎、前端 UI、小程序性能優(yōu)化等方面有豐富的實(shí)踐經(jīng)驗(yàn)。
版權(quán)聲明:本文內(nèi)容由互聯(lián)網(wǎng)用戶自發(fā)貢獻(xiàn),該文觀點(diǎn)僅代表作者本人。本站僅提供信息存儲空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如發(fā)現(xiàn)本站有涉嫌抄襲侵權(quán)/違法違規(guī)的內(nèi)容, 請發(fā)送郵件至 舉報(bào),一經(jīng)查實(shí),本站將立刻刪除。