Hermes將成為React Native默認的JS引擎

語言: CN / TW / HK

自 2019 年首次發佈以來,小巧輕便的 JavaScript 引擎 Hermes 在社區中的名氣越來越高,很多的框架也開始支持Hermes。作為 React Native 領域高人氣元框架的締造者,Expo 團隊此前公佈了對 Hermes 的實驗性支持。另外,流行移動數據庫 Realm 團隊近期也決定為 Hermes 提供 alpha 支持。

在本文中,我們希望重點介紹過去兩年來在推動 Hermes 成為 React Native 最佳 JavaScript 引擎方面取得的各項激動人心的進展。展望未來,我們有信心通過更多改進讓 Hermes 成為各類平台上 React Native 中的默認 JavaScript 引擎。

專為 React Native 而優化

Hermes 中的功能定義,負責指示要如何提前執行編譯工作。換言之,啟用 Hermes 的 React Native 應用程序會附帶經過預編譯優化的字節碼,而非純 JavaScript 源代碼。這就大大減少了用户啟動產品所需要的工作量。來自 Facebook 及社區其他應用的量化測試表明,啟用 Hermes 通常能夠將產品的 TTI(即交互時間)指標縮短近一半。

但我們不會止步於此,始終致力於對 Hermes 進行全方位改進,努力讓它成為最出色的 React Native 專用 JavaScript 引擎。

為 Fabric 建立新的垃圾收集器

在新一代 React Native 架構中嶄露頭角的Fabric 渲染器可謂萬眾矚目,它能夠在 UI 線程上同步調用 JavaScript。但如果 JavaScript 線程的執行時間過長,則會導致明顯的 UI 丟幀、令用户無法正常輸入。

React Fiber 提供的併發渲染機制能夠將渲染工作拆分成多個塊,由此避免單一 JavaScript 任務佔用過長時間。此外,JavaScript 線程當中還有另一大常見延遲來源——垃圾收集(GC)機制。因為一旦開始垃圾收集,整個 JavaScript 引擎必須放下手頭的所有工作去執行垃圾收集。

Hermes 當中的原有默認垃圾收集器GenGC 屬於單線程分代垃圾收集方案。其中會對新生代採用典型的半空間複製策略,而對老年代則使用 mark-compact 策略、從而更主動將內存返還至操作系統。

在像 Facebook for Android 這樣的複雜應用上,我們觀察到的平均暫停時長為 200 毫秒,而第 99 百分位暫停則為 1.4 秒。考慮到 Facebook for Android 龐大且多樣化的用户羣體,最極端的暫停時間甚至長達 7 秒。

為了緩解這種情況,我們建立起全新的、以併發為主要取向的垃圾回收方案,即Hades。Hades 同樣採用分代設計,其新生代回收方式與 GenGC 完全相同,而老年代回收方式則通過快照式標記掃描收集器進行管理。

Hades 能夠將大部分工作負載交由後台線程執行,從而顯著縮短垃圾回收暫停時長,同時不會阻止引擎主線程繼續執行 JavaScript 代碼。我們的統計數據顯示,Hades 在 64 位設備上第 99.9 百分位上的延遲為 48 毫秒(比 GenGC 快 34 倍!),而在 32 位設備上第 99.9 百分位上的延遲約 88 毫秒(以單線程增量 GC 的形式運行)。

但由於需要資源成本更高的寫屏障、速度更慢的基於空閒列表的分配機制(與碰撞指針分配器相反)以及更多的堆碎片,Hades 實際是在用整體吞吐量來換取更短的暫停時間。我們認為這樣的取捨符合用户習慣,也將通過合併與接下來將要討論的其他內存優化機制,實現更低的整體內存佔用量。

改善性能問題

應用程序的啟動時長對很多應用產品來説至關重要,我們也希望不斷提升 React Native 的性能上限。對於在 Hermes 當中實現的一切 JavaScript 功能,我們都會認真監測它們對生產性能造成的影響,並確保它們不會拉低性能指標。

在 Facebook,我們目前正在為 Metro 中的 Hermes 試驗一個專用的 Babel transform profile ,希望用 Hermes 中的原生 ESNext 實現替換掉原本的十餘種 Babel transform。通過這種方式,我們已經在直接觀察中將 TTI 改進了 18% 至 25%,整體字節碼獲得顯著瘦身;希望接下來也能在 OSS 中得到類似的結果。

除了啟動性能之外,我們還將內存佔用量視為改進 React Native 應用程序的重要機會,這也是成就良好虛擬現實體驗的前提。因此在 JavaScript 引擎的底層控制當中,我們利用壓縮位與字節實現了多輪內存優化:

  1. 此前,所有 JavaScript 值都被表示為 64 位 NaN 裝箱編碼的標記值形式,用以表示 64
    位架構上的雙精度浮點值與指針。但這種方式在實踐中屬於巨大浪費,因為大多數數字實際都屬於小整數(SMI),而且客户端應用程序的JavaScript 堆通常不會大於 4 GiB。為了解決這個問題,我們引入了一種新的 32 位編碼,其中 SMI 與指針以 29位編碼(因為指針為 8 字節對齊,可以假設底部 3 位始終為零),其餘的 JS 數字則裝箱在堆上。如此一來,JavaScript的堆大小就縮減了約 30%。
  2. 不同類別的 JavaScript 對象在 JS 堆上表示為不同的 GC 管理單元。通過主動優化這些單元頭的內存佈局,我們得以將內存佔用量進一步削減約 15%。

對此,我們在 Hermes 中還做出一項關鍵決定,即不再採用即時(JIT)編譯器。因為我們認為對於大多數 React Native 應用程序來説,額外的預熱成本與額外的二進制文件乃至內存佔用量並沒有實際意義。

多年以來,我們在解釋器性能與編譯器的優化方面投入了大量精力,也讓 Hermes 獲得了遠超其他引擎的 React Native 工作負載吞吐量優勢。我們將繼續關注廣泛存在的性能瓶頸(解釋器調度循環、堆棧佈局、對象模型、垃圾回收等)以提高吞吐量。

垂直整合領域

在 Facebook,我們習慣於使用大型 monorepo 託管項目。通過將引擎(Hermes)與 host(React Native)緊密迭代在一起,現在我們為垂直整合開闢出廣闊空間。以下是幾個具體的例子:

  • Hermes 使用 Chrome DevTools 協議支持使用 Chrome 調試器在設備上執行 JavaScript 調試。這種方法比傳統“遠程 JS 調試”(使用應用內代理在桌面 Chrome 上運行 JS)效果更好,因為它支持調試同步本機調用並能保證統一的運行時環境。與 React DevTools、Metro 以及 Inspector 等一道,Hermes 調試器現已成為 Flipper 中的組成部分,共同提供良好的一站式開發者體驗。
  • 在 React Native 應用的初始化路徑中分配的對象往往長期存在,而且並不符合分代 GC 所提出的分代假設。因此,我們在 React Native 中配置 Hermes 時,會將前 32 MiB 直接分配至老年代(即 pre-tenuring)以避免觸發 GC 暫停與延遲 TTI。
  • 新的 React Native 架構在很大程度上基於 JSI(即 JavaScript Interface),這是一種輕量級通用 API,用於將 JavaScript 引擎嵌入至 C++ 程序當中。通過讓維護 JS 引擎的團隊同時維護 JSI API 實現,我們有信心提供最佳集成效果。而且這套集成方案已經在 Facebook 的大規模業務之上經過實戰測試,擁有良好的可靠性與運行效率。
  • 擁有語義正確且性能良好的 JavaScript 併發原語(例如 promises)及平台併發原語(例如 microtasks),對於 React 併發渲染以及 React Native 應用程序的未來發展可謂至關重要。從歷史上看,React Native 中的 promise 是使用非標準化 setImmediate API 作為膩子腳本。我們正努力通過 JSI 實現來自 JS 引擎的原生 promises 和 microtasks,並將 queueMicrotask(Web 標準中的新增項目)引入平台,從而更好地支持現代異步 JavaScript 代碼。

社區發展

Facebook 公司非常重視 Hermes 項目,但只有為 Hermes 建立起完整的生態系統、特別是技術社區,開發工作才算真正吿一段落。也只有這樣,每個人才能充分運用 Hermes 的功能併發揮其潛力。

擴展至更多新平台

Hermes 最初僅面向 React Native on Android 開源。在此之後,我們很高興看到社區成員們逐漸拓展 Hermes 的支持範圍,目前已經將其擴展到 React Native 生態系統 所覆蓋的多種其他平台。

Callstack 率先在 React Native 0.64 當中將Hermes 引入 iOS。他們還發布了系列專題文章,併發起播客向用户們介紹他們如何實現這一目標。根據基準測試,與 Mattermost 應用的 JSC 相比,Hermes 在 iOS 上的啟動性能可穩定提升約 40%、內存佔用量減少約 18%、應用程序運行期間的內存用量僅為 2.4 MiB。

微軟則不斷將Hermes 引入 Windows 與 MacOS 上的 React Native。在微軟 Build 2020 大會上,軟件巨頭表示相較於原本的 Chakra 引擎,Hermes 能夠將 React Native for Windows 的內存佔用量降低 13%。而在最近的一些綜合基準測試中,微軟發現 Hermes 0.8(包含 Hades 及之前提到的 SMI 與指針壓縮優化功能)佔用的內存量比其他引擎少 30% 至 40%。毫無疑問,基於 React Native 的桌面版 Messenger 視頻通話體驗也在 Hermes 的支持下得到顯著改善。

更重要的是,Hermes 還一直在為 Oculus 上使用 React 系列技術構建的各類虛擬現實體驗提供支持,其中也包括 Oculus Home。

社區支持

我們承認,目前 Hermes 身上仍有一些問題阻礙着更多社區的順暢介入,我們也在努力為這些缺失的功能建立支持。我們的目標是儘快實現功能完備,讓 Hermes 成為大多數 React Native 應用程序的最佳選擇。以下是社區正在籌劃的 Hermes 發展路線圖:

  • 由於 Facebook 並不使用,所以Proxy 與 Reflect 最初被排除在 Hermes 之外。我們當時擔心即使不真正使用,貿然添加 Proxy 也會損害屬性查找性能。但隨着MobX 與Immer 等庫的流行,Proxy 很快成為 Hermes 當中最受歡迎的功能。經過認真評估,我們決定針對社區提供專用 Proxy,而且設法以極低的成本完成實現。由於 Facebook 並不使用此功能,所以只能依靠技術社區證明其穩定性。我們首先在 0.4 與 0.5 版本中以標記和創建 opt-in npm 包的形式啟動了 Proxy 測試,並從0.7 版本開始將其默認啟用。
  • ECMAScript 國際化 API 規範 (簡稱 ECMA-402 或 Intl)同樣是用户呼聲中的焦點。Intl 代表一組龐大的 API,通常需要包含 6 MB 大小的 Unicode CLDR 數據才能實現。正因為如此,FormatJS(又名 react-intl)等膩子腳本以及 社區 JSC 的國際變體 build 等 JS 引擎才如此臃腫笨拙。為了避免 Hermes 二進制文件體積的不必要膨脹,我們決定直接使用並映射操作系統內置庫所提供的 ICU facilities 來實現,相應的代價就是給某些跨平台行為引入一些(通常較為微小的)差異。
  • 微軟合作完成了 Android 上的 build 支持工作。其中幾乎涵蓋從 ECMA-402 到 ES2020 的所有內容,而對體積的影響僅有 3%(每個 ABI 僅為 57 K 到 62 K)。我們在Twitter 上發起的民意調查發現,用户們強烈支持默認包含 Intl,所以我們決定從 0.8 版本開始引入這項功能。
  • Facebook 已經贊助 Major League Hacking 發起遠程開源獎學金計劃。去年,我們推出了 Hermes 採樣分析器;今年,我們的研究員將與 Hermes、React Native 以及 Callstack 的成員們合作,在 iOS 上實現對 Hermes Intl 的支持。
  • 感謝大家反饋中提到的默認堆大小上限太低問題,導致很多不熟悉自定義 Hermes GC 配置的用户會產生不必要的GC 壓力乃至OOM 崩潰。因此在默認情況下,我們將上限由 512 MiB 增加到 3 GiB,這樣的配置對大多數用户來説應該綽綽有餘。
  • 有報吿稱,我們專用的 Function.prototype.toString 實現會導致庫執行不正確的特徵檢測並導致性能下降,而且令用户無法執行源代碼注入。在解決問題的同時,我們也更加堅定了 Hermes 應該尊重事實、儘量避免妨礙開發者順暢使用的決心。

總結

總之,我們的願景是讓 Hermes 成為一切 React Native 平台上的默認 JavaScript 引擎。我們正在朝着這個方向努力,也希望充分聽取大家來自不同角度的反饋意見。

只有做好萬全準備,我們才能為生態系統廣泛接納 Hermes 奠定堅實的基礎。在這裏,我們誠邀大家體驗 Hermes,並將您發現的一切建議、意見、功能請求與不兼容性錯誤提交給我們的 GitHub repo。

原文鏈接: Toward Hermes being the Default