五年電商前端團隊小程式開發經驗總結(萬字原理 + 優化乾貨)
highlight: a11y-dark theme: geek-black
一、探討一下雙執行緒架構
對微信小程式底層設計的疑問 - 雙執行緒模型到底是什麼? - 如何將檢視和邏輯分開? - 為什麼要用 WXML、WXSS 這種奇怪的語法? - 如何遮蔽掉危險操作 API? - 如何進行通訊,資料驅動檢視更新如何實現? - 雙執行緒模型有何缺點?
1、模型組成
1、傳統瀏覽器程序、執行緒模型存在的問題
效能問題
渲染執行緒與 JS 引擎執行緒是互斥的,存在阻塞問題,長時間的 JS 指令碼執行直接會影響到 UI 檢視的渲染,導致頁面卡頓
安全問題
危險的 HTML 標籤(a、script)和 JS API(Function、eval、DOM API),攻擊者很容易對頁面內容進行篡改,非法獲取到使用者資訊等
解決方案
小程式採用了雙執行緒模型,將檢視展示和邏輯處理分開在兩個執行緒中執行
模型中對 HTML 標籤進行了封裝,開發者無法直接呼叫原生標籤,JS 中原有的危險邏輯操作 API 能力也被遮蔽,涉及到的通訊動作只能通過中間層提供的 API 去觸發虛擬 DOM 更新(資料驅動檢視)
2、小程式雙執行緒模型
其中檢視層及邏輯層的能力都是通過微信客戶端內建的小程式基礎庫 JS 檔案來提供的
WAWebview.js 提供檢視層基礎的 API 能力,包括一整套元件系統及 Virtual Dom(虛擬DOM)的相關實現
WAService.js 提供邏輯層基礎的 API 能力,向開發者暴露各類操作的 API
3、客戶端引擎底層實現
不同執行環境下,指令碼執行環境以及用於元件渲染的環境是不同的,效能表現也存在差異:
-
在 iOS、iPadOS 和 Mac OS 上,小程式邏輯層的 JavaScript 程式碼執行在 JavaScriptCore 中,檢視層是由 WKWebView 來渲染的,環境有 iOS 14、iPad OS 14、Mac OS 11.4 等
-
在 Android 上,小程式邏輯層的 JavaScript 程式碼執行在 V8 中,檢視層是由基於 Mobile Chromium 核心的微信自研 XWeb 引擎來渲染的(從過去的 X5 換成了自研的 XWeb 引擎,檢視層是基於 Mobile Chrome 核心來渲染的,而且根據官方披露,將來邏輯層也會脫離 V8,使用定製化的 XWeb Worker 執行緒,即自定義一個 Web Worker 執行緒,作為小程式的邏輯層。參考連結)
-
在 Windows 上,小程式邏輯層 JavaScript 和檢視層都是用 Chromium 核心
-
在 開發工具上,小程式邏輯層的 JavaScript 程式碼是執行在 NW.js 中,檢視層是由 Chromium WebView 來渲染的
2、檢視層
1、小程式元件系統
小程式中會涉及到的元件共有三種類型:內建元件、自定義元件、原生元件
2、小程式元件組織框架
1、Web Component
Web Component 是瀏覽器中建立封裝功能和定製元素的能力,由三項主要技術組成
-
Custom elements(自定義元素) 一組 JavaScript API,允許定義 custom elements 及其行為,然後可以在使用者介面中按照需要使用它們
-
Shadow DOM(影子DOM) 一組 JavaScript API,用於將封裝的“影子” DOM 樹附加到元素(與主文件 DOM 分開呈現)並控制其關聯的功能。通過這種方式保持元素的功能私有,這樣它們就可以被指令碼化和樣式化,而不用擔心與文件的其他部分發生衝突
-
HTML templates(HTML模板)
template 和 slot 元素可以編寫不在呈現頁面中顯示的標記模板。然後它們可以作為自定義元素結構的基礎被多次重用
2、Exparser 框架
Exparser 是微信小程式的元件組織框架,內建在小程式基礎庫中,為小程式的各種元件提供基礎的支援 小程式內的所有元件,包括內建元件和自定義元件,都由 Exparser 組織管理
小程式中,所有節點樹相關的操作都依賴於 Exparser,包括 WXML 到頁面最終節點樹的構建、 createSelectorQuery 呼叫和自定義元件特性等
Exparser 會維護整個頁面的節點樹相關資訊,包括節點的屬性、事件繫結等,相當於一個簡化版的 Shadow DOM 實現
3、Exparser 特點
-
基於 Shadow DOM 模型:模型上與 Web Component 的 Shadow DOM 高度相似,但不依賴瀏覽器的原生支援,也沒有其他依賴庫;實現時,還針對性地增加了其他 API 以支援小程式元件程式設計
-
可在純 JS 環境中執行:這意味著邏輯層也具有一定的元件樹組織能力(在元件的渲染中就會用到)
-
高效輕量:效能表現好,在元件例項極多的環境下表現尤其優異,同時代碼尺寸也較小
4、Exparser 核心方法
registerBehavior 註冊元件的一些基礎行為,供元件繼承
registerElement 註冊元件,跟我們互動介面主要是屬性和事件
元件建立
小程式基礎庫對外提供有 Page 和 Component 兩個構造器,小程式啟動將 properties、data、methods 等定義欄位,寫入 Exparser 的元件登錄檔,在初始化頁面時, Exparser 會創建出頁面根元件的一個例項,用到的其他元件也會相應建立元件例項(這是一個遞迴的過程)
5、Page 和 Component 的建立過程
從上圖中我們能夠比較明顯的看到小程式在 Page 渲染和 Component 渲染時通訊方式上存在差別。其中,Page 渲染中 VDOM 的生成和 diff 都是在檢視層完成的,邏輯層只負責對 data 資料的傳送,來觸發渲染層的更新邏輯。而在 Component 的渲染中,邏輯層和檢視層需要共同維護一套 VDOM,方式則是在元件初始化時在邏輯層構建元件的 VDOM,然後將其同步到檢視層。後續的更新操作則會先在邏輯層進行新舊 VDOM 的 diff,然後僅將 diff 之後的結果進行通訊,傳遞到檢視層之後直接進行 VDOM 的更新和渲染。這樣做最大的好處就是將檢視更新通訊的粒度控制成 DOM 級別,只有最終發生改變的 DOM 才會被更新過去(因為有時候 data 的改變並不一定會帶來檢視的更新),相對於之前 data 級別的更新會更加精準,避免非必要的通訊成本,效能更好。
3、小程式原生元件
1、原生元件的優點
-
擴充套件 Web 的能力。比如像輸入框元件(input, textarea)有更好地控制鍵盤的能力。
-
體驗更好,同時也減輕 WebView 的渲染工作。比如像地圖元件(map)這類較複雜的元件,其渲染工作不佔用 WebView 執行緒,而交給更高效的客戶端原生處理。
-
渲染效能更好,繞過 setData、資料通訊和重渲染流程,比如像畫布元件(canvas)可直接用一套豐富的繪圖介面進行繪製。
2、原生元件建立過程
-
元件被建立後插入到 DOM 樹裡,渲染一個佔位元素,瀏覽器核心會立即計算佈局,此時我們可以讀取出元件相對頁面的位置(x, y 座標)、寬高
-
元件通知客戶端,客戶端在相同的位置上,根據寬高插入一塊原生區域,之後客戶端就在這塊區域渲染介面
-
當位置或寬高發生變化時,元件會通知客戶端做相應的調整
3、原生元件渲染限制
-
一些 CSS 樣式無法應用於原生元件
-
層級問題(原生元件層級最高,會覆蓋 WebView 層的元件)
解決方式
-
使用 cover-view 和 cover-image 元件覆蓋原生元件(能力有限,不靈活)
-
同層渲染(通過一定的技術手段把原生元件直接渲染到 WebView 同一層級上)
4、原生元件同層渲染
1、iOS 系統實現原理
小程式 iOS 端的同層渲染是基於 WKChildScrollView 實現的,原生元件在 attached 之後會直接掛載到預先建立好的 WKChildScrollView 容器下
原理分析
WKWebView 在內部採用的是分層的方式進行渲染,WKWebView 會將 WebKit 核心生成的 Compositing Layer(合成層)渲染成 iOS 上的一個 WKCompositingView 元件,但合成層與 DOM 節點之間不存在一對一的對映關係(核心一般會將多個 DOM 節點合併在一個合成層中)。
WKChildScrollView:當把一個 DOM 節點的 CSS 屬性設定為 overflow: scroll (低版本需同時設定 -webkit-overflow-scrolling: touch)之後,WKWebView 會為其生成一個 WKChildScrollView(為了可以讓 iOS 上的 WebView 滾動有更流暢的體驗,WebView 裡的滾動實際上是由真正的原生滾動元件來承載的,WebKit 核心已經處理了它與其他 DOM 節點之間的層級關係),並且 WKChildScrollView 與 DOM 節點存在一對一對映的關係。
建立流程
-
建立一個 DOM 節點並設定其 CSS 屬性為 overflow: scroll 且 -webkit-overflow-scrolling: touch
-
通知客戶端查詢到該 DOM 節點對應的原生 WKChildScrollView 元件
-
將原生元件掛載到該 WKChildScrollView 節點上作為其子 View
-
通過上述流程,小程式的原生元件就被插入到 WKChildScrollView 了
2、Android 系統實現原理
WebPlugin 是瀏覽器核心的一個外掛機制,小程式 Android 端的同層渲染是基於 embed 標籤和 WebPlugin 實現的
原理分析
embed 標籤是 HTML5 新增的標籤,是使用來定義嵌入的內容(如外掛、媒體等),格式可以是 PDF、Midi、Wav、MP3 等等,比如 Chrome 瀏覽器上的 PDF 預覽,就是基於 embed 標籤實現的。
基於 embed 標籤結合 Chromium 核心擴充套件來實現,Chromium 支援 WebPlugin 機制,WebPlugin 是瀏覽器核心的一個外掛機制,主要用來解析和描述 embed 標籤,將原生元件渲染到核心提供的 Texture 紋理上
建立流程
-
WebView 側建立一個 embed DOM 節點並指定元件型別
-
Chromium 核心會建立一個 WebPlugin 例項,並生成一個 RenderLayer
-
Android 客戶端初始化一個對應的原生元件
-
Android 客戶端將原生元件的畫面繪製到步驟 2 建立的 RenderLayer 所繫結的 SurfaceTexture 上
-
通知 Chromium 核心渲染該 RenderLayer
-
Chromium 渲染該 embed 節點並上屏
5、檢視層程式碼編譯
微信開發者工具內建了二進位制的 WXML 和 WXSS 程式碼檔案編譯器,編譯器接受 WXML 和 WXSS 程式碼檔案列表,處理完成之後輸出 JavaScript 程式碼
1、WXSS 程式碼檔案
wcsc 命令工具執行過程
-
wcsc 編譯 WXSS 生成一個 JS 檔案產物
-
JS 檔案包含新增尺寸單位 rpx 轉換方法,可根據裝置螢幕寬度自適應計算成 px 單位
-
提供 setCssToHead 方法將轉換後的 css 內容新增到 html 模版的 head 中
-
eval 方法執行這個 JS 檔案字串完成樣式注入
2、WXML 程式碼檔案
WCC 命令工具執行過程
-
wcc 編譯 WXML 生成一個 JS 檔案產物
-
JS 檔案包含 $gwx 方法,接收 WXML 檔案路徑,執行後生成 generateFunc 方法,並觸發 generateFuncReady 事件
-
generateFunc 方法接受動態資料 data,類似於一個 render 函式,用於生成 virtual dom
3、邏輯層
1、邏輯層包含內容
-
提供 JS 程式碼執行環境
-
為 window 物件新增模組介面 require define
-
提供 Page,App,getApp 等介面
-
提供 wx 全域性物件下面的 api 方法,網路、媒體、檔案、資料快取、位置、裝置、介面、介面節點資訊還有一些特殊的開放介面
2、JS 程式碼執行環境
1、JSVirtualMachine
它通過例項化一個 VM 環境來執行 JS 程式碼,如果你有多個 JS 需要執行,就需要例項化多個 VM。並且需要注意這幾個 VM 之間是不能相互互動的,因為容易出現 GC 問題
2、JSContext
JSContext 是 JS 程式碼執行的上下文物件,相當於一個 Webview 中的 window 物件。在同一個 VM 中,你可以傳遞不同的 Context
3、JSValue
和 WASM 類似,JsValue 主要就是為了解決 JS 資料型別和 Swift 或 Java 資料型別之間的相互對映。也就是說任何掛載在 JSContext 的內容都是 JSValue 型別,Swift 和 Java 在內部自動實現了和 JS 之間的型別轉換
4、JSExport
是 JSCore 裡面,用來暴露 Native 介面的一個 protocol。簡單來說,它會直接將 Native 的相關屬性和方法,直接轉換成 prototype object 上的方法和屬性
3、模組規範的實現
- 通過 define 定義一個模組,限制了模組可使用的其他模組,如 window,document 等,define 方法則是通過將所有的 JS 程式碼邏輯都以“路徑-模組”的鍵值對的方式存在了全域性變數裡實現的
- 使用 require 來應用一個模組,使用模組時只會傳入 require 和 module、exports,程式碼中讀取到的其他變數就會都是 undefined,這也是不能在小程式中獲取一些瀏覽器環境物件的原因
這其實就是典型的模組載入思路,與 Webpack 打包模組的處理方式十分相似
4、通訊原理
WAWebview.js 和 WAService.js 檔案中除了提供檢視層、邏輯層基礎的 API 能力之外,還提供了這兩個執行緒之間進行通訊的能力
小程式邏輯層和渲染層的通訊會由 Native (微信客戶端)做中轉,邏輯層傳送網路請求也經由 Native 轉發
1、底層實現
幾個端的不同實現最終會封裝成 WeiXinJSBridge 這樣一個相容層供給開發者呼叫
1、iOS 端
檢視層是通過 WKWebView 的 window.webkit.messageHandlers.NAME.postMessage 和 eval 實現。邏輯層是往 JavaScripCore 框架注入一個全域性的原生方法
2、Android 端
檢視層和邏輯層的實現原理一致,都是往 WebView 的 window 物件注入一個原生方法 WeixinJSCore 實現,這個 WeixinJSCore 是微信提供給 JS 呼叫的介面(native實現)
3、微信開發者工具
使用 websocket 進行通訊
2、WeixinJSBridge 模組物件
WeixinJSBridge提供了檢視層 JS 與 Native、檢視層與邏輯層之間訊息通訊的機制,提供瞭如下幾個方法:
-
invoke:JS 呼叫 Native API
-
on:JS 監聽 Native 訊息
-
publish:檢視層釋出訊息
-
subscribe:訂閱邏輯層的訊息
5、雙執行緒架構的缺陷和優化
1、雙執行緒架構的缺陷
-
不能靈活操作 DOM,無法實現較為複雜的效果
-
部分和原生元件相關的檢視有使用限制,如微信的 scrollView 內不能有 textarea
-
頁面大小、開啟頁面數量都受到限制
-
需要單獨開發適配,不能複用現有程式碼資源
-
在 JSCore 中 JS 體積比較大的情況下,其初始化時間會產生影響
-
傳輸資料中,序列化和反序列化耗時需要考慮
2、雙執行緒架構的優化
雙執行緒帶來的效能瓶頸,也是微信自身一直致力於解決的關鍵問題。前面提到小程式將會在新的 XWeb 核心裡,將邏輯層的實現替換為通過修改 Chromium 核心,實現自定義的 XWeb Worker 執行緒,這樣就不再需要額外的 V8 了,使得記憶體佔用方面有顯著減少。另外,由於是基於 Chromium 核心的 Worker 執行緒,所以關於資料通訊這塊,很自然的也會擁有 PostMessage 的能力,來替代原有的 setData 底層通訊方式,獲得更高效能的通訊能力
3、支付寶小程式的雙執行緒架構
另外,我還了解研究了支付寶小程式底層的相關實現。支付寶小程式也使用了類似的雙執行緒架構模型,通過使用 UC 提供的瀏覽器核心,渲染層是在 Webview 執行緒中執行,邏輯層則是另起一個執行緒執行 Service Worker。但是 Service Worker 需要通過 MessageChannel API 來與 渲染執行緒通訊,當資料量較大、物件較複雜時同樣存在效能瓶頸。
因此支付寶小程式重新設計了現有的 JS 虛擬機器 V8,提出了一種優化的隔離模型(Optimized isolation model, OIM)。OIM 的主要思路是共享 JS 虛擬機器例項中與執行緒執行環境無關的資料和基礎設施,以及不可變或不易變的 JS 物件,使得在保持 JS 層邏輯隔離的前提下,節省多例項場景下在記憶體和功耗上的開銷。儘管有些例項間共享的資料會帶來同步的開銷,但是在隔離模型下,本方案所共享的資料、物件、程式碼和虛擬機器基礎設施都是不可變或者不易變的,所以很少發生競爭。
在新的隔離模型下,Webview 裡面的 V8 例項就是一個 Local Runtime,Worker 執行緒裡面的 V8 例項也是一個 Local Runtime,在邏輯層和渲染層互動時,setData 物件的會直接建立在 Shared Heap 裡面,因此渲染層的 Local Runtime 可以直接讀到該物件,並且用於渲染層的渲染,減少了物件的序列化和網路傳輸,極大的提升了啟動效能和渲染效能。
除此之外,支付寶小程式實現了首頁離線快取優化,首先渲染上次儲存下面的首頁 UI 頁面,把首頁展現給使用者,然後在後臺繼續載入前端框架和業務的程式碼,載入完成後再和離線快取的首頁 UI 進行合併,給使用者展現動態的首頁,這一點和微信小程式的初始渲染快取方案十分類似且更加激進。還使用了 WebAssembly 技術重新實現了虛擬 DOM 這塊的核心程式碼,提升了小程式的頁面渲染。
6、對雙執行緒模型架構設計的思考
技術架構為解決問題而生,一個好的技術方案不僅需要設計者在開發效率、技術成本、效能體驗、系統安全之間做出平衡和取捨,還需要與業務走向、產品形態、使用者訴求緊密結合
二、執行時機制一探究竟
1、程式碼包下載
在小程式啟動(冷啟動)時,微信會為小程式展示一個固定的啟動介面,介面內包含小程式的圖示、名稱和載入提示圖示
此時,微信會在背後完成幾項工作:下載小程式程式碼包、載入小程式程式碼包、初始化小程式首頁
從開發者的角度看,控制程式碼包大小有助於減少小程式的啟動時間。對低於 1MB 的程式碼包,其下載時間可以控制在 929ms(iOS)、1500ms(Android) 內
優化方案
-
減小主包、分包大小
-
合理分配,分包預載入規則
-
精細拆分,非同步化分包
2、頁面層級準備
在小程式啟動前,微信會提前準備好一個頁面層級用於展示小程式的首頁。每當一個頁面層級被用於渲染頁面,微信都會提前開始準備一個新的頁面層級,使得每次呼叫 wx.navigateTo 都能夠儘快展示一個新的頁面。檢視層頁面內容都是通過 pageframe.html 模板來生成
頁面層級的準備步驟
-
第一階段是啟動一個 WebView,在 iOS 和 Android 系統上,作業系統啟動 WebView 都需要一小段時間
-
第二階段是在 WebView 中初始化基礎庫,此時還會進行一些基礎庫內部優化,以提升頁面渲染效能
-
第三階段是注入小程式 WXML 結構和 WXSS 樣式,使小程式能在接收到頁面初始資料之後馬上開始渲染頁面(這一階段無法在小程式啟動前執行)
3、頁面程式碼注入和執行
1、初始化全域性變數
檢視層全域性變數
- __wxConfig:是根據全域性配置和頁面配置生成的配置物件,包含 page、分包 page、tabbar 等資訊
- __wxAppCode__:非JS型別的開發者程式碼
- JSON:JSON 配置檔案內容
- WXML:$gwx 函式的執行結果,是一個可執行函式,執行後將會返回 VDOM 物件
- WXSS:eval 執行樣式函式的結果,是一個可執行函式,執行後插入 style 標籤
邏輯層全域性變數
- __wxConfig:與檢視層類似,不包含分包 page、tabbar 等資訊
- __wxAppCode__:與檢視層類似,不包含 WXSS 型別資料
- __wxRoute: 用於指向當前正在載入的頁面路徑
- __wxRouteBegin: 用於標誌 Page 的正確註冊
- __wxAppCurrentFile__: 用於指向當前正在載入的 JS 檔案
- __wxAppData: 小程式每個頁面的 data 域物件
- Component: 自定義元件構造器
- Behavior: 自定義元件 behavior 構造器
- definePlugin: 自定義外掛的構造器
- global: 全域性物件
- WeixinWorker: 多執行緒構造器
2、基礎庫和業務程式碼JS檔案注入
檢視層程式碼注入
- 注入並執行 wxml.js 檔案(定義 $gwx 方法)
- 注入並執行 wxss.js 檔案(eval 方法,新增 app.wxss 樣式檔案)
- 新增 __wxAppCode__ 變數
- 執行 eval 方法,插入當前頁面及引用元件樣式檔案
- 執行 $gwx(當前頁面路徑),返回對應的生成虛擬 DOM 的方法
邏輯層程式碼注入
- 注入並執行 JS 檔案(順序:其他、babel、Component、Page、App)
- 注入並執行wxml.js檔案(定義$gwx方法)
按需注入和用時注入
通常情況下,在小程式啟動時,啟動頁面所在分包和主包(獨立分包除外)的所有 JS 程式碼會全部合併注入。自基礎庫版本 2.11.1 起,配置了 按需注入和用時注入 ,小程式僅注入當前頁面需要的自定義元件和頁面程式碼,在頁面中必然不會用到的自定義元件不會被載入和初始化
3、邏輯層業務程式碼執行
4、資料通訊和檢視渲染
1、頁面初始渲染
資料通訊
在小程式啟動或一個新的頁面被開啟時,頁面的初始資料(data)和路徑等相關資訊會從邏輯層傳送給檢視層,用於檢視層的初始渲染。
Native 層會將這些資料直接傳遞給檢視層,同時向用戶展示一個新的頁面層級,檢視層在這個頁面層級上進行介面繪製。
檢視層接收到相關資料後,根據頁面路徑來選擇合適的 WXML 結構,WXML 結構與初始資料相結合,得到頁面的第一次渲染結果
在完成檢視層程式碼注入,並收到邏輯層傳送的初始資料後,結合從初始資料和檢視層得到的頁面結構和樣式資訊,小程式框架會進行小程式首頁的渲染,展示小程式首屏,並觸發首頁的 onReady 事件。
如果開啟了 初始渲染快取,首次渲染可以直接使用渲染層的快取資料完成,不依賴邏輯層的初始資料,降低啟動耗時(onReady 事件提前執行)。
頁面初始化的時間大致由頁面初始資料通訊時間和初始渲染時間兩部分構成。 其中,資料通訊的時間指資料從邏輯層開始組織資料到檢視層完全接收完畢的時間,資料量小於 64KB 時總時長可以控制在 30ms 內。
傳輸時間與資料量大體上呈現正相關關係,傳輸過大的資料將使這一時間顯著增加。 因而減少傳輸資料量是降低資料傳輸時間的有效方式。
初始渲染
初始渲染髮生在頁面剛剛建立時。初始渲染時,將初始資料套用在對應的 WXML 片段上生成節點樹。節點樹也就是在開發者工具 WXML 面板中看到的頁面樹結構,它包含頁面內所有元件節點的名稱、屬性值和事件回撥函式等資訊。最後根據節點樹包含的各個節點,在介面上依次創建出各個元件。初始渲染中得到的 data 和當前節點樹會保留下來用於重渲染。
在這整個流程中,時間開銷大體上與節點樹中節點的總量成正比例關係。因而 減少 WXML 中節點的數量 可以有效降低初始渲染和重渲染的時間開銷,提升渲染效能。
2、更新資料渲染
初始渲染完畢後,檢視層可以在開發者呼叫 setData 後執行介面更新。
setData 原理
1、改變對應的 this.data 的值(同步操作)
解析屬性名(包含 . 和 [] 等資料路徑符號),返回相應的層級結構,修改對應的區域性 data 的值
js
{abc: 1} 中 abc 屬性名 => [abc]
{a.b.c: 1} 中 'a.b.c' 屬性 => [a,b,c]
{"array[0].text": 1} => [array, 0, text]
2、將資料從邏輯層傳送到檢視層(非同步操作)
evaluateJavascript:使用者傳輸的資料,需要將其轉換為字串形式傳遞,同時把轉換後的資料內容拼接成一份 JS 指令碼,再通過執行 JS 指令碼的形式傳遞到兩邊獨立環境
重渲染時,邏輯層將 setData 資料合併到 data 中,渲染層將 data 和 setData 資料套用在 WXML 片段上,得到一個新節點樹。然後將新節點樹與當前節點樹進行比較,這樣可以得到哪些節點的哪些屬性需要更新、哪些節點需要新增或移除。最後,用新節點樹替換舊節點樹,用於下一次重渲染。
在進行當前節點樹與新節點樹的比較時,會著重比較 setData 資料影響到的節點屬性。因而,去掉不必要設定的資料、減少 setData 的資料量也有助於提升這一個步驟的效能。
3、使用者事件通訊
檢視層會接受使用者事件,如 點選事件、觸控事件 等。
使用者事件的通訊比較簡單,當一個使用者事件被觸發且有相關的事件監聽器需要被觸發時,檢視層會將資訊反饋給邏輯層。
因為這個通訊過程是非同步的,會產生一定的延遲,延遲時間同樣與傳輸的資料量正相關,資料量小於 64KB 時在 30ms 內。
降低延遲時間的方法主要有兩個
-
去掉不必要的事件繫結(WXML 中的 bind 和 catch),從而減少通訊的資料量和次數
-
事件繫結時需要傳輸 target 和 currentTarget 的 dataset,因而不要在節點的data字首屬性中放置過大的資料
三、小程式基建和優化方案實踐
1、小程式效能優化
1、程式碼分包
1、合理的分包和分包預載入策略
主包內容
Tab頁(系統要求)、業務必要頁面(錯誤兜底頁、登陸授權頁等),其餘檔案都以業務模組或者頁面的維度,拆分成各自的分包
分包預載入
據使用者的實際使用場景,預測下一跳的頁面,將可能性最高的場景設定為預載入分包(可以參照業務埋點資料),例如:進入電商首頁後,需要對會場和商詳頁的分包進行預載入
2、元件分包分發
實現思路
小程式不支援主包引用分包程式碼,只能在分包中引用主包程式碼,所以把公共使用的元件程式碼放在主包目錄中,但這些公共元件未必在主包所屬的頁面中會被引用,可能只是在分包頁面中被多次引用,這樣使得主包中程式碼體積越來越大,使用者首次載入速度變慢。
將主包頁面不依賴的公共元件分別分發到依賴它們的分包目錄中,雖然分包各自的體積會有所增大,但主包體積會有顯著下降
實現原理
將所有需要分發的元件放置主包指定目錄中,並新增配置檔案,說明組建檔案分發資訊。在開發時用 gulp 任務監聽單個檔案變化、在構建時遞迴遍歷所有元件,將其複製到配置檔案中指定的子包路徑目錄中。
目標檔案在複製之前,都先要將檔案內的依賴路徑進行更新,使其在子包中執行時也能引用成功。針對不同型別的檔案,採取不同的依賴分析手段。
-
JS 檔案:使用 babel.transformFile 修改依賴引用地址
-
WXSS 檔案:使用 postcss.plugin('transform-wxss') 處理依賴的 @import 匯入樣式檔案地址
-
WXML 檔案:使用 require('htmlparser2').Parser 來轉換內部 wxs、template(import 和 include 匯入)依賴的引用地址
非同步分包
小程式基礎庫版本 2.17.3 及以上開始支援分包非同步化,即可以在分包之間互相引用,這個能力可以快速取代我們自己的元件分發方案,而且優化效果更佳,可以將分包中公共依賴的程式碼部分打成新的分包,在使用時非同步按需引入,能力與 Web 端的非同步元件類似,但這個方案在生產環境的穩定性有待驗證。
2、setData 優化
實現思路
合併 setData 呼叫,將其放在下個時間片(事件迴圈)統一傳輸,降低通訊頻率
實現原理
需要先將邏輯層 this.data 進行更新,避免前後資料不一致造成邏輯出錯。將需要傳送至檢視層的 data 進行整合,在 nextTick 中呼叫原生的 setData 統一進行傳送,可以有效降低通訊頻率,並且在傳送前手動做一次與 this.data 的 diff 操作,降低通訊體積
1. 降低頻率
js
const nextTick = wx.nextTick ? wx.nextTick : setTimeout; // 小程式裡的時間片 API
2. 減少體積
參考京東 Taro 中 diff 的實現,對基本、陣列、物件等不同型別進行處理,最終轉換為 arr[1]、x.y 這樣的屬性路徑語法,減少傳輸資訊量
3、Storage API 重封裝
實現思路
onLaunch、onLoad 等生命週期函式中存在大量對微信 Storage 的同步呼叫(使用 Sync 結尾的API),這些操作涉及JS與原生通訊,同步等待耗時過久,推遲頁面 onReady 觸發即使用者可互動時間,影響使用者體驗。直接改為非同步操作又存在業務程式碼改動量較大的問題,存在一定風險,大量的非同步回撥程式碼語義不優雅、可讀性較差。因此需要對原生 Storage 操作進行重封裝,改為對記憶體中物件的實時存取,提高響應速度,並定期呼叫原生 API 向真實 Storage 中同步。
實現原理
封裝呼叫方式一致的 API,以 getStorage 和 getStorageSync 為基礎API,首次呼叫時觸發原生 API 獲取原始資料,獲取到之後儲存至記憶體物件,後面的各種操作(刪改查)便都基於這個物件做操作。其中的 set、remove 操作需要對對應的資料做髒標誌並存儲到一張髒表裡,以便在後面將變動同步到原生端。呼叫方需要定時呼叫變動同步方法來持久化資料(遍歷同步髒表中的資料),以防記憶體執行時資料意外丟失,一般需要定期執行(app 的 onShow 執行 setInterval)和在 app 的 onHide 生命週期執行。
我們不僅對 Storage API 進行了重封裝,對於其他耗時較久的同步 API(例如用來獲取裝置和系統資訊的 getSystemInfo/getSystemInfoSync API,底層都是同步實現),我們也採用了類似的方式,僅在第一次獲取時呼叫系統 API 並將結果快取在記憶體中,之後的呼叫則直接返回快取資訊。
4、Data Prefetch
1、釋出訂閱方式
實現思路
從 A 頁面跳轉到 B 頁面之前,在 A 頁面 emit 訊息,觸發 B 頁面資料介面請求,並快取結果資料,當 B 頁面開啟時,先取快取,取不到再重新呼叫介面
實現原理
B 頁面雖然沒有例項化,但是頁面已經被註冊,Page 外層程式碼已被執行,因此可以事先與 A 頁面完成訊息的釋出訂閱
2、全域性註冊變數方式
實現思路
當需要預載入的頁面沒有在主包或者是非同步註冊時,無法通過釋出訂閱的模式進行預請求,我們可以通過全域性註冊預載入方法的方式來實現
實現原理
在前一頁面路由跳轉時發起預請求,將此請求儲存到一個全域性的 Promise 變數裡,在需要預載入的頁面首次渲染時優先去取全域性的 Promise 變數 then 回撥裡的資料結果
5、記憶體優化
1、圖片優化
對整體的圖片資源做優化(阿里雲函式)
- 尺寸:實際寬高的 1-2 倍寬度
- Webp 格式:Android 端全量使用
- Png 格式:透明底強訴求場景才使用,其餘預設使用 Jpg/Jpeg
- 圖片懶載入:開啟 Image 元件的 lazy-load 屬性
2、低端機降級
實現思路
根據使用者機型硬體效能層級,前端展示設定不同的呈現策略
實現原理
低端機場景下對以下功能進行降級
- gif 動圖進行降級成靜態圖片
- JS、CSS 動畫降級成靜態樣式
- 不展示 swiper 元件
- 質量使用 60% 的 quality
3、長列表優化
- RecycleView
- IntersectionObserver
2、小程式工程化
1、外掛化框架
框架工具
BeautyWe 是一個三方小程式業務開發框架,通過對生命底層公共業務邏輯的封裝
應用場景
公共業務邏輯,例如每個頁面對使用者登入狀態的校驗、獲取和處理 url 引數、PV 自動埋點、效能監控等
實現原理
生命週期函式外掛化,將原生的 onLoad、onShow、onReady 等函式使用 Object.defineProperty 的方式進行重寫,使其內部形成一個 Promise 任務鏈,通過引入外掛,向任務鏈中前後位置自由插入方法,在系統生命週期函式執行時觸發並依次呼叫。
實現生命週期鉤子的外掛化之後,我們便可以將各類需要處理的底層公共邏輯進行封裝,將其插入到宿主(原生生命週期)的 Promise 任務鏈中
2、自動化部署
基於小程式 CI + Gitlab CI + Puppeteer 實現了一套半自動的小程式構建部署工具,詳見我的另一篇文章《微信小程式自動化部署方案》,後面微信小程式提供了可以在 Linux 環境單獨執行的 miniprogram-ci,更加簡單易用。
3、埋點監控
監控能力
- 業務埋點(頁面 PV、模組曝光等業務埋點)
- 效能上報(小程式啟動、頁面渲染、FMP、記憶體告警)
- 異常監控(JS錯誤、介面錯誤、業務錯誤)
效能監控上報型別
-
memory_warning:記憶體告警資料上報。收集方法:wx.onMemoryWarning 回撥中收集告警等級
-
app_launch:app 執行時資料上報。收集方法:記錄 app 生命週期執行時間(onLaunch、onShow),在 page 的 onLoad 時上報
-
page_render:page 執行時資料上報。收集方法:記錄 page 生命週期執行時間(onLoad、onShow、onReady),在 page 的 onHide 或 onUnload 時上報
實現原理
基於外掛化框架的方案,對生命週期鉤子函式做重寫,即可在頁面程式執行過程中自動記錄和上報效能資料
四、總結思考與發展前景
基建能力不斷完善
官方文件中對框架底層技術的不斷披露,說明小程式技術建設日益成熟完善。基建生態的不斷豐富,同層渲染、網路環境監測、初始渲染快取、啟動效能優化,官方陸續提供了這些能力的支援,讓小程式與 Web 生態逐步靠攏。
我們自身在業務迭代中,為了解決各種問題,自己造了很多輪子,例如:分包非同步化、CI 打包、效能 Performance API,這些能力後面微信都原生給予實現。看上去似乎是做了無用功,但事實上恰恰說明我們過去做的事情是正確的,方向也是和整個生態的發展是相吻合的。
更多場景賦能業務
除了技術能力的完善之外,微信小程式生態還在不斷豐富更多業務場景下的能力支撐,例如支援小程式分享朋友圈、支援生成短連結、微信聊天素材開啟小程式,為我們業務提供更多可能性與想象力。
微信還推出了小程式硬體框架,能讓硬體裝置(非通用型計算裝置)在缺乏條件執行微信客戶端的情況下執行微信小程式,在各行各業的安卓系統平板電腦、大屏裝置等硬體,提供低成本螢幕互動解決方案,為物聯網裝置使用者提供更標準化、功能豐富的使用體驗。
這不禁讓我想起那句流傳了數十年的計算機技術領域 Atwood 定律,任何可以用 JavaScript 來編寫的應用,最終都將用 JavaScript 來編寫。那麼同樣的,任何可以用小程式來實現的產品,最終都將用小程式來實現。
小程式發展前景展望
自從 2017 年微信小程式誕生以來,超級 App + 小程式/輕應用的這種模式在各種業務中屢試不爽,例如微信和微信小程式、支付寶和支付寶小程式、抖音和抖音小程式等等,小程式這種成本低、迭代快、易推廣的產品模式,在超級 App 帶來的巨大流量加持下,在各個領域都獲得了巨大的成功,並且已經逐漸成為一種趨勢,越來越多的公司在自己的產品中加入了這種模式。小程式本身也已經在不知不覺中融入到了生活點滴中,疫情期間各個地區的健康碼應用,小區裡的電商團購、飯店點餐的小程式碼,想喝奶茶咖啡在小程式上提前下單,人們的生活已經與小程式緊不可分,小程式這一產品技術方案確確實實創造了極大的社會價值。
我曾經與朋友聊天時戲稱小程式是具有中國特色的 PWA 應用,面向未來小程式仍然大有可為,除了現有佔比較高的的網路購物和生活服務類應用,在更多的場景下也都能看到小程式技術的用武之地,例如在健康醫療、線下零售、娛樂遊戲、AI 智慧等行業領域內遠遠沒有達到飽和的狀態,市場空白讓小程式的開發潛力變得更大,小程式正向著當初設立的目標 讓小程式觸手可及,無處不在 不斷邁進。