寫在前面
大家好,我是
染陌,這是我在
全球開源技術峰會 GOTC上的一個 topic ——《基于 Flutter 的 Web 渲染引擎「北海 Kraken」》。我主要從技術角度來分享 Kraken 的一些實作原理以及關鍵的技術特性,現在整理成文字版分享給大家。
Kraken Github:
https://github.com/openkraken/krakenKraken 官網:
https://openkraken.com/北海的技術背景
說到北海的技術背景就不得不提及跨端技術的演進,很多同學應該都比較熟悉跨端技術的曆程了,我還是簡單講一些。
我們知道,浏覽器是最成熟的天然跨平台方案。早在 PC 時代,浏覽器已經成為了網際網路的入口,大家都會習慣性通過浏覽器來進行網頁的浏覽以汲取各種資訊,當時我們把這種上網的方式叫做“沖浪”。然而到了移動時代,浏覽器在移動裝置上并沒有一個搶眼的表現,反之因為記憶體大、弱網環境白屏久、傳感器能力缺失(标準跟進慢)等問題使各種質疑不絕。
為了彌補上述浏覽器在移動端的一些不足,出現了 Hybrid 技術,在 Web 之上通過容器的能力實作一些非标準化的超集,同時也通過 prefetch、離線包等各種技術來提升首屏的加載性能。
此後,出現了類 RN 的方案(典型代表 React Native),它的原理是通過 JS engine 将 Native 控件與前端生态實作一個橋接,通過 Web 開發業務邏輯提升效率,而向下通過 Native 控件渲染來提升性能及體驗。但是這類方案的缺點是無法完全抹平兩端的差異,沒有解決一緻性的問題,而最終将複雜度暴露給了開發者。

Flutter 作為跨端屆的新寵,這兩年也獲得了越來越多的關注,下面介紹一下 Flutter。
Flutter 的優點是性能好、由于其通過自繪渲染使得跨端一緻性高,但是它也有它自身的缺點,比如生态自成一派,既不是前端也不是 Android/iOS。
這就是引出了一系列的問題。
- 首先,前端(JavaScript)或用戶端(Swift / JAVA)轉型都有一定成本,但是由于端側的 GUI 體系大同小異。筆者站在一個前端開發者的視角去看語言上的學習成本并不會特别高,有 React 或者 Vue 等前端架構經驗的同學可以通過簡單的學習快速上手。對于一些小型的創業團隊,确實可以小步快跑快速學習上手并開發,但當組織龐大到一定程度,這個轉換的成本将會指數級上升。
- 其次,生态圈等待重建立設,一些 Flutter 開發者朋友或許覺得目前 Flutter 開發已經有挺多的 pub 可以直接使用了。但實際上生态圈不止于 Flutter pub,還有各種已有的基礎鍊路,比如建設相關的 CI/CD,再比如搭建等等。這一系列的生态都需要重建立設,成本是非常大的。
- 再次,已有的非常多業務都是通過 JavaScript + 前端架構開發的前端項目,我們如果想把它們遷移成 Dart + Widget 成本無疑是非常龐大的。
在面對如此多的問題以及切換的高成本的同時,我們也期望通過 Flutter 給我們的業務帶來更多的技術的可能性,同時改善 Web 容器在端上的一些性能及體驗問題。那麼,引入一項新技術的第一步是解決引入這項新技術的成本問題,是以我們積極探索一種将前端生态與 Flutter 結合起來的方案。
于是産生了本次 topic 的主角——北海 Kraken。
Kraken 是一款高性能 Web 标準的自繪渲染引擎,具有高性能、易擴充、基于 Flutter 以及 遵守 Web 标準的特點。
下面我列舉了一些北海在阿裡的一些應用場景,在 C 端 APP 或者 IoT 裝置上,北海都有相關的落地。
北海的技術原理
在介紹 Kraken 的技術原理之前,我先示範一下如何開發一個 Kraken 應用。因為 Kraken 是基于 W3C 标準來開發的 Web 渲染引擎,是以上層是架構無關的,無論開發者使用的是 Vue 或者 React 還是 Rax 都可以在 Kraken 上進行一個應用開發。
以 Vue.js 開發為例,下面是我用 Vue 官方提供的 vue-cli 起的一個項目。具體的代碼見
官方示例。
可以看到的是,最左邊是 Vue 的相關代碼,右邊分别是該應用在 Chrome(左)上跑的結果以及在 Kraken(右)上跑的結果,大家可以看到結果是完全一緻的。
了解了如何開發一個 Kraken 應用 ,我們再來了解一下 Kraken 的技術原理。為了大家更好地了解,首先我們來比較一下 Flutter 于 Webview 的渲染流程。
WebView 的渲染流程相信大家非常熟悉了,面試中非常經典的題目就是一個 URL 輸入如何最終渲染到螢幕上了。總的來說就是解析 HTML、JS 以及 CSS 檔案,執行相應 JS 調用 DOM API,最終會生成 DOM Tree 以及 CSSOM Tree,然後會計算最終得到 Render Tree,經過 Layout 以及 Paint 流程生成一系列的 Layer,最終通過合成以及光栅化渲染到螢幕上。
再看 Flutter 這邊,Flutter 經典的三棵樹——Widget Tree、Element Tree 以及 RenderObject Tree。Widget Tree 對應到前端類似于前端架構這層,而 Element 與 DOM Tree,RenderObject Tree 與 Render Tree 分别對應,最終也會通過 Layout 以及 Paint 一系列計算生成 Layer,然後通過合成以及光栅化渲染到螢幕上。
那麼,我們再将前端架構加入到我們整個流程中進行一個更加直覺的對比,這裡還是以 Vue.js 為例。
Vue.js 會在運作時生成一系列 Vdom 産生 Vdom Tree,再通過 platfom 的抽象調用具體平台的 API。
那麼我們就會發現,隻需要把我用紅框圈出來的部分的流程進行互換,就可以實作我們最終想要實作的效果(上層 Web 開發,下層基于 Flutter 進行渲染)。
基于以上設想,那麼北海的渲染流程就出來了。
目前主流的前端架構都會将産物打成一個 JS Bundle,通過标準的 DOM API 去操作具體的視圖,而 HTML 内一般隻有一個根結點。在 Web 下,頁面會先請求 HTML 檔案,再解析 Script 标簽去加載對應的 JS 檔案。而 Kraken 的入口設計成了一個 JS 檔案,這樣做可以減少一次請求,加快首屏的渲染。
該 JS 檔案會在 JS Engine 中執行,Kraken 的 runtime 通過 JS Engine Binding 的方式提供了一系列 Web 标準的 API 接口,調用相應 API 會執行相關邏輯并建立一系列需要發送給 Dart 層處理的指令,指令通過一個 struct 進行存儲。C++ 通過 FFI 将相應的指令底層的 address 發送到 Dart 這邊,Dart 處理相關指令并生成 Dom Tree。同樣的,CSS 也會通過 Parser 生成對應的 CSSOM Tree,最終會結合生成 Flutter 的 RenderObject Tree,經過 Layout 以及 Paint 的一系列計算,生成對應的一系列 Layter,然後通過合成光栅化最終上屏顯示。
同樣的,在最新的實作中,我們考慮到了 SSR 應用的場景,是以加入了 HTML 為入口的北海應用開發方式,通過 HTML Parser 即可解析對應的 HTML 檔案,後續流程是一樣的。SSR 的支援也讓首屏的秒開率更上一層樓。
那麼了解了 Kraken 的整個渲染流程,那麼我們如何基于 Flutter 去完成 Web 标準的渲染引擎的開發呢?
那麼要基于 Flutter 去做這個事情,就必須先了解 Flutter 的架構。
Flutter 最上層是 Dart 實作的 Framework,包含了響應式架構、官網元件庫以及實作布局與繪制協定的部分。中間是 C++ 實作的 Flutter Engine,他是渲染流程的下半部分,提供了一些基礎能力,以及将 layer 合成以及光栅化後輸出。最下層的 Embedder 層,則負責具體 platform 的一些實作,以實作跨平台。
不難發現,最 Dart Framework 的 Widget 是對 UI 的抽象,實作了一套響應式架構,對應到前端就是 Vue / React 等前端架構。而下方的布局協定,可以對應 W3C 的标準來實作一套基于前端标準的布局與繪制協定。
那麼我們就可以得出北海的架構設計。
先看左邊,左邊還是上面介紹的 Flutter 的整體架構,Flutter 的 Widget 能力可以通過插件的形式注冊到 Kraken 中去,成為一個前端标準的 Tag,JS 可以動态化地調用及控制渲染。整個左側的 Flutter 架構支撐了上層的 Flutter 生态,使 Flutter 生态也可以通過插件的形式融合到整個 Kraken 的渲染體系中去。
右邊是 Kraken 的架構實作,Kraken 的實作并沒有把實作侵入到 Flutter Engine 中去。在 Dart 層,通過實作 W3C 标準的一系列布局與渲染能力,為上層提供了一些列标準化的能力,比如 Element、CSS、以及各種 Web 标準的 Module 等。在上層 Kraken 的 runtime 通過 JS Engine Binding 的方式提供了一系列 Web 标準的 API 接口,調用相應 API 會執行相關邏輯并建立一系列需要發送給 Dart 層處理的指令,指令通過一個 struct 進行存儲。C++ 通過 FFI 将相應的指令底層的 address 發送到 Dart 這邊,最終 Dart 根據指令調用前面說的标準化能力,以完成對接。通過該實作,為上層的前端生态提供了支撐,憑借豐富的前端生态,開發者可以享受前端生态帶來的高效的開發體驗。
關鍵技術特性
首屏的加載性能是一個 C 端場景的關鍵名額,長時間的白屏會極大地影響使用者體驗。
Kraken 在 首屏初始化時需要建立大量的節點,大量的時間耗費在通信上,是以優化首屏性能迫在眉睫。
在上面技術原理部分我們知道,Kraken 需要通過 Bridge 來完成 C++(JS Engine) 與 Dart 之間的通信,以達到将指令傳遞到 Dart 層的目的,Bridge 的架構也進行了三個版本的演進。
最初的第一代方案,我們侵入了 Flutter Engine,使資料從 JS Engine 傳遞到 Flutter Engine 中,然後通過 native bingding 最終将資料發送給 Dart 層。這一代的方案非常明顯的缺點是侵入了 Flutter Engine,開發時需要編譯 Flutter Engine 需要耗費大量的時間。同時,對于 Kraken 的架構來說,侵入 Flutter Engine 也并不是一個合理的設計。
後來出現了 Dart FFI,可以實作 C++ 與 Dart 之間的高效通信,是以産生了第二代方案。第二代 Bridge 方案通過将 JSON 資料序列化後,通過 Dart FFI 将資料傳遞到 Dart 層,Dart 層再通過 JSON 的反序列化以拿到最終的資料。這代方案比起上一代方案可以解決侵入 Flutter Engine 的缺點,但是引入了字元串的拷貝以及 JSON 序列化反序列化的時間長的問題。
為了解決上述問題,于是産生了第三代 Bridge 方案。第三代 Bridge 方案通過共享記憶體的方式定義了一個标準的 40 Bytes 的 Struct 來存儲指令,而通過 Dart FFI 傳遞的隻是指令的位址,C++ 跟 Dart 兩邊都依賴位址來通路相關資料。這樣做解決了 JSON 序列化反序列化的問題,節約了時間,并且少一次資料拷貝。同時,由于記憶體是 40 Bytes 對齊的,可以提高記憶體的通路效率。
下面是一些實際線上頁面帶來的首屏收益。
無限滾動的長清單是困擾前端開發者很久的曆史性問題了,大量的 layout 導緻頁面卡頓,以及滾動時 Paint 的時間長導緻滾動掉幀,頁面的體驗非常糟糕。社群也有非常多的前端的解決方案來處理該問題,而在 Kraken 上,我們也期望在容器層解決該問題。
在 Android 跟 iOS 上也分别有 RecyclerView 以及 TableView 來解決該問題,他們的原理分别是在可視區域 viewport 外定義一塊緩沖區域,當節點超過該區域時進行動态釋放,進入該區域時動态建立,以及通過一系列節點進行屬性替換的方式來保證節點數不爆炸。Flutter 中也提供了類似實作 Sliver,那麼我們能否用 Sliver 賦能前端解決該問題呢?
Kraken 定義了一個新的
display
屬性
sliver
,通過将節點的
display
屬性設定為
sliver
,則可以直接使用 Flutter 的 Sliver 能力,以達到節點超出可視及緩存區域後動态回收的一個能力。可以看到我們使用 1000 個卡片的 DEMO 進行測試,
sliver
下比起
block
有明顯的收益。
同時,該标準也已經在
W3C 中文興趣小組進行了讨論,期望在大家讨論充分以及達成共識後,嘗試将此提案向 W3C 進行送出,反哺前端社群。
一個大前端團隊往往既有用戶端也有前端,會沉澱一系列的端上的能力。不同的需求會有不一樣的技術選型,譬如說一個播放器往往是通過 Native 技術去開發的。我們期望将端上的能力(包括 Flutter Widget、Web、Native 以及三方 SDK 等)進行整合,融合成一個大前端的端開發體系,是以在 Kraken 内我們如何整合端上的這一系列能力呢?同時,我們也期望按需引入,能做到包體積的優化。在不同的業務域,我們期望可以快速地進行定制化開發,快速形成一套垂直業務域的領域能力。
Kraken 提供了一套擴充能力來解決上述問題,通過渲染能力擴充接口,開發者可以将開發完成的符合标準的 Flutter Widget 以及 Native 的渲染能力快速內建到 Kraken 體系中去,最終通過 JavaScript 來提供一個動态化調用的能力。同樣的,通過 MethodChannel,開發者可以通過該通道調用一些 Native 或者 Dart API 的能力,譬如說一些二方或者三方的 SDK 能力。
開發者可以通過擴充能力自定義業務域需要的能力,按需拔插以達到包體積優化的目的。同樣的,注冊到 kraken 的插件都可以通過 JavaScript 代碼控制,提供了動态性。
下面是一系列在 Kraken 内部擴充 Flutter Widget、Native API 以及 Native 播放器的 Demo。
下面是提升可互動性。在介紹 Kraken 的可互動性之前,我們先來看一下在 Web 下的一些互動問題。
在 Web 下開發富互動能力的應用時,前端開發者往往需要引入一個額外的 lib 來提供增強的手勢能力(譬如說 Hammer.js 這樣的手勢庫)。那麼目前端開發者引入 lib 時,就會導緻加載 index.html 以後,還需要額外的請求對應的 JS 庫,造成一次額外的請求開銷的同時延長了首屏的可互動時間。
當使用者在螢幕上進行某個操作時,由于使用者操作的方式可能是使用者的手,也可能是 Apple Pencil 或者滑鼠這樣的裝置。是以在 W3C 标準中,将使用者操作可互動應用的觸點抽象為一個
pointer,這些 pointer 會根據操作形成一個手勢,分别是 down、move、up 三個過程,其中 move 可省略(譬如說 click)。
在 Web 中,需要将這一系列 pointer 給 dispatch 到 element tree 上,通過冒泡将這些 pointer 頻繁地發送到 JS 層,然後 JS 再通過封裝 Touch API 來完成對互動的識别。這樣做帶來幾個問題,首先頻繁地将 pointer 從 C++ 傳遞到 JS 帶來了不必要的開銷,此外封裝标準的能力也會造成額外的開發成本,易用性并不突出。此時,如果使用社群的一些方案,也會導緻非标準化使标準不對齊導緻同個應用中的不同頁面有不一緻的互動體驗。
為了解決上述問題,我們期望從标準化、易用性、标準化幾個方面提供一套标準化的互動能力。通過封裝底層的 pointer 來得到不同的手勢能力,使開發者可以快速開發富互動的應用。
下面是 Kraken 中增強互動能力的流程圖。當使用者進行某些互動操作以後,每一個觸點的 pointer 會從 Native 傳遞到 Kraken 中,Pointer 會同時分發給 GestureManager(手勢識别器管理類)以及 Scroll 識别器。GestureManager 會識别開發者通過 Web 标準的監聽行為(EventTarget.addEventListener)來注冊以及分發給對應的手勢識别器,同樣 Scroll 識别器也會被分發 pointer。這些識别器被加入到 Flutter 的競争場進行手勢競争,以保證隻觸發某一個具體操作(互動可控)。Scroll 識别器會觸發滾動區域的滾動操作,手勢識别器則會通過标準的 Web 流程進行冒泡以及 dispatch,最終開發者通過監聽事件完成自定義行為。
開發應用時,調試能力是必不可少的,前端開發效率高不止要歸功于繁榮的生态,友好的開發調試體驗一樣是提升效率的神器。
Kraken 抽象了 Inspector 以通過 Chrome DevTools Protocol 來對接 Chrome DevTools,提供了一系列跟前端開發 Web 應用完全一緻的調試體驗,無論開發者喜歡使用 Console.log 還是通過 JS Debugger ,都可以快速上手。
此外,Kraken 也通過支援 HMR 的所有标準的 Web API,來提供局部熱更新的能力,使開發 Kraken 應用能跟 Web 下一緻的局部熱更新的調試體驗,大大提升了開發者的開發調試體驗。
最後,Kraken 的所有代碼都已經開源,Kraken 提供了開放的 TSC 機制期望所有開發者可以平等地交流以及決策,使 Kraken 可以更好地發展,也歡迎更多的開發者一起來共建 Kraken。