在 Hermes 成為 React Native 預設 JS 引擎的路上

語言: CN / TW / HK

感謝印記中文的@QC.L 對本文進行翻譯。這篇文章的英文原文 Towards Hermes being the Default 於今年 10 月 26 日釋出於 React Native 的官方部落格上。對我個人來說,很有幸能夠代表 Hermes 與 React Native 團隊與社群聊聊我們對於 JavaScript 引擎的一些想法。第一次以 Facebook/Meta 工程師身份寫英文宣告還是既有挑戰又興奮得,所以也想要親自分享給大家。

這裡也感謝前端之巔公眾號在第一時間就釋出了一版翻譯。不過由於我本人並不知曉,所以沒能參與任何校隊與潤色工作,在內容上還是希望大家還是以這版為準。

2019 年我們釋出 Hermes 以來 ,它在社群已經獲得了越來越多的認可。比如,React Native 應用程式元框架團隊Expo 就在最近宣佈了對 Hermes 的實驗性 支援,這一度是 Expo 論壇上呼聲最高的功能之一 。移動端資料庫團隊Realm 也在近期對 Hermes 提供了 alpha 支援 。本文將重點介紹過去兩年以來,我們在推動 Hermes 成為 React Native 最佳 JavaScript 引擎方面取得的最令人興奮的一些成果。展望未來,我們有信心通過進一步的改進,使 Hermes 成為 React Native 在所有平臺上的預設 JavaScript 引擎。

為 React Native 優化

Hermes 的核心特性是如何將編譯工作提前進行(ahead-of-time),這意味著啟用 Hermes 的 React Native 應用會攜帶預編譯的優化後位元組碼,而非原始的 JavaScript 原始碼。這極大地減少了使用者啟動產品所需的工作量。由 Facebook 以及社群應用的測量資料表明,啟用 Hermes 往往能將產品的 TTI(全稱Time-To-Interactive,即可互動時間)指標減少近一半。

但我們並不想止步於此,我們一直在對 Hermes 進行全方位的改進,致力於讓其在作為 React Native 專用 JavaScript 引擎方面更加出色。

為 Fabric 構建的全新 GC

隨著 React Native 新架構中 Fabric 渲染器的推出,它使得在 UI 執行緒中同步呼叫 JavaScript 成為可能。然而,這意味著如果 JavaScript 執行緒的執行時間過長,就會出現非常明顯的 UI 掉幀,並且會阻塞使用者的輸入。ReactFiber 啟用的併發渲染 將通過把渲染工作分片來避免排程過長的 JavaScript 任務。但是,在 JavaScript 執行緒中還有一個非常常見的延遲來源,那就是在 JavaScript 引擎不得不 “停止一切” 以進行垃圾回收(GC)時。

之前 Hermes 中預設的垃圾回收器是GenGC,它是一款單執行緒的分代式垃圾回收器。新生代採用了典型的半區複製(semi-space copying)策略,而老生代則使用了標記整理(mark-compact)策略,使其特別擅於將未使用的記憶體返還給作業系統。但由於是單執行緒,GenGC 存在導致長時間 GC 暫停的缺陷。在類似安卓版 Facebook 這樣複雜的應用程式上,我們觀察到平均暫停時間為 200ms,p99 大概是 1.4s。在安卓版 Facebook 龐大且多樣的使用者群體中,甚至還曾達到 7s 之久。

為了解決這個問題,我們實現了一個全新且 高併發 的 GC,名為Hades。Hades 回收新生代的方式與 GenGC 完全一致,但它採用原始快照(SATB)式標記擦除(mark-sweep)回收器來管理老生代。它可以通過在後臺執行緒中執行大部分工作,而不會阻塞引擎主執行緒執行 JavaScript 程式碼,來顯著減少 GC 的暫停時間。 根據我們統計資料顯示,Hades 在 64 位裝置上 p99.9 的暫停時間僅為 48ms(比 GenGC 要快 34 倍!) ,並且在 32 位上 p99.9 的暫停時間約為 88ms(此時它會作為一個單執行緒 增量 CG 執行)。這些暫停時間的改進以整體吞吐量為代價,因為需要更昂貴的寫屏障,更慢的 freelist 分配(相對於使用 bump pointer 分配),甚至還會額外增加堆的碎片化程度。但我們認為這都是正確的取捨,通過合併(coalescing)以及其他接下來會討論到的記憶體優化機制,我們最終其實達到了更低的整體記憶體佔用。

攻克效能痛點

App 的啟動時間對於許多 App 的成功來說至關重要,我們希望能不斷提升 React Native 的上限。對於在 Hermes 中實現的任何 JavaScript 功能,我們都會仔細監控它在生產環境對效能造成的影響,確保它們不會倒退任何指標。在 Facebook,我們目前正在 Metro(React Native 使用的 bundler)中試驗為 Hermes 提供一個專用的 Babel 轉換配置檔案 來用 Hermes 原生的 ESNext 實現替換掉十多個 Babel 轉換。我們的內部資料顯示 TTI 有 18-25% 的提升 ,同時 整體位元組碼的大小也隨之減少 ,我們目測在開源環境也能有類似的改進效果。

除了啟動效能外,我們還注意到記憶體佔用也是 React Native 需要改進的痛點,特別是在VR 場景下的記憶體佔用。得益於我們作為 JavaScript 引擎所擁有的底層控制能力,我們能夠從二進位制層面提供諸多記憶體方面的優化:

  1. 之前,所有的 JavaScript 值都會被表示為 64 位 NaN-boxing 編碼的標記值,用以表示 64 位架構上的雙精度浮點數和指標。但這在實踐過程中非常浪費資源,因為大多數數字其實都是 SMI(小整數,全稱 Small Integer),並且客戶端應用的 JavaScript 堆一般也不會超過 4GiB。為了解決此問題,我們引入了全新的 32 位編碼,其中 SMI 和指標都會被編碼為 29 位(因為指標會以 8 位元組對齊,我們可以假設底部 3 位都是 0),而其餘的 JS 數字都會被裝箱到堆中。 這個優化最終使得 JavaScript 堆大小整體減少了 30% 左右
  2. 不同種類的 JavaScript 物件在 JavaScript 堆中被表示為不同種類的 GC 管理單元。通過對這些單元標頭檔案的記憶體佈局進行壓榨, 我們能夠再減少近 15% 的記憶體佔用

我們對 Hermes 的一大關鍵舉措是不實現JIT 編譯器,因為我們堅信對於大多數 React Native 應用來說,額外的預熱開銷以及對二進位制檔案與記憶體佔用的增加並不值得。多年以來,我們在直譯器效能優化和編譯器優化方面投入了大量精力,以使 Hermes 的吞吐量在 React Native 的負載風格上能與其他引擎不相伯仲。我們將繼續通過專注於解決各方面效能瓶頸(直譯器排程迴圈、堆疊佈局、物件模型、GC等)來進一步提高吞吐量。敬請期待!

垂直整合先驅

在 Facebook,我們傾向於把專案整合在一個大的monorepo 中,所以引擎(Hermes)與宿主環境(React Native)是一起迭代得,這使得我們有很多空間去做垂直整合。舉例來說:

  • Hermes 遵循了Chrome DevTools 協議,因此,它支援 用 Chrome 偵錯程式對裝置上的 JavaScript 進行除錯 。它比傳統的 “遠端 JS 除錯”(使用應用內代理到桌面端 Chrome 中執行 JS)更好,因為它支援有同步的原生呼叫的場景,並且能保證與真機一致的執行時環境。Hermes 除錯工具與 React DevTools,Metro,Inspector 等一併成為Flipper 的一部分,為大家提供了一站式開發方案。
  • 在 React Native 應用的初始化過程中分配的物件往往是長期存在的,並且不會遵循分代 GC 所利用的分代假說。因此,我們 在 React Native 中配置 Hermes 時 ,會將前 32MB 直接分配到老生代(稱為 pre-tenuring ),以避免觸發 GC 暫停造成 TTI 的延遲。
  • 新 React Native 架構在很大程度上是基於 JSI (即 JavaScript Interface) 實現的,這是一個輕量級的通用 API,主要用於將 JavaScript 引擎嵌入到 C++ 程式中。介於我們的 JSI API 整合實現是由我們 JS 引擎團隊自己維護得,所以我們有信心能提供最正確與效能最好的實現,而且是在 Facebook 的規模上實戰檢驗過得。
  • 讓 JavaScript 併發原語(例如,promises)和平臺併發原語(例如微任務(microtasks))語義正確同時兼具高效能,對於 React 併發渲染和 React Native 應用的未來顯得至關重要。過去,React Native 中的 Promise 是基於非標準化的 setImmediate API 實現的 polyfill 。我們正在努力將 JS 引擎的原生 Promise 和微任務通過 JSI 實現,並在平臺上引入 queueMicrotask ,這是最近引入的 web 標準,以更好地支援現代非同步 JavaScript 程式碼。

帶動整個生態

Hermes 對 Facebook 來說足夠好用。但是,我們的工作遠不止於此,我們的終極目標是讓整個社群能都夠使用 Hermes,這樣我們才能讓整個生態一起向前,並真正發掘出 Hermes 的潛力。

開拓到新的平臺

Hermes 起初只為 Android 上的 React Native 開放了原始碼。從那以後,我們看到社群成員將 Hermes 的支援擴充套件到 React Native 生態系統所能擴充套件到的諸多平臺之上

在 React Native 0.64 中,由Callstack 牽頭完成了將 Hermes 引入 iOS 平臺的工作 。他們編寫了系列文章 並主持了播客 來介紹他們的實現過程。從他們提供的跑分結果來看,與 JSC 相比,Hermes 在 iOS 上能為 Mattermost (一個開源的 React Native 應用)穩定提供近 ~40% 的啟動優化同時減少了近 ~18% 的記憶體佔用 ,而應用程式只增加了 2.4 MiB 開銷。眼見為實,推薦你去看下原文。

微軟則一直在推進將 Hermes 引入 React Native for Windows 和 React Native for macOS 中。 在微軟 Build 2020 大會 上,微軟分享了 Hermes 的記憶體佔用(工作集)比 React Native for Windows 中的 Chakra 引擎低 13%。另外,在最近的一些測試跑分中,他們發現(使用 Hades GC 並且包含了上文提到的 SMI 與指標壓縮優化的)Hermes v0.8 版本, 在記憶體佔用上比其他引擎少近 30%-40% 。你可能已經猜到了,Messenger 桌面端應用 基於 React Native 構建的影片通話體驗,也是跑在 Hermes 上得。

值得一提得是,所有 Oculus 上基於 React 技術構建的虛擬現實體驗,包括 Oculus Home(一開啟的主屏),也都是由 Hermes 在底下驅動得。

支援我們的社群

我們知道目前仍有不少問題阻礙了部分社群採用 Hermes,我們承諾將會補上這些坑。我們的目標是讓 Hermes 功能足夠齊全到可以滿足併成為大多數 React Native 應用程式的選擇。Hermes 的路線其實已經在被社群影響了,比如:

  • Proxy 和 Reflect 最初未被 Hermes 實現,因為 Facebook 並不使用它們。我們曾擔心新增 Proxy 會損害整個屬性查詢的效能(即便程式碼中沒有用到)。但由於MobX 和Immer 等庫的流行,Proxy 很快成為了 Hermes 呼聲最高的功能 。經過慎重評估後,我們決定專門為了社群去實現 Proxy,而且我們最終找到了對效能代價極小的方式來實現。由於這是我們不使用的功能,所以我們只能依靠社群來證明其穩定性。我們在 v4.0v0.5 中提供了可選(opt-in)Proxy 支援的 npm 包。並最終在 v0.7 起預設啟用 Proxy
  • ECMAScript 國際化 API 規範(ECMA-402,也稱為 Intl)呼聲第二高的功能Intl 是一組龐大的 API,通常需要實現包含 6MB 大小 的Unicode CLDR 資料。這就是為什麼類似於 FormatJS (a.k.a. react-intl) 的 polyfills 以及像社群為 JSC 構建的國際版 JS 引擎如此龐大的原因。為了避免大幅增加 Hermes 的二進位制大小,我們決定通過直接訪問與對映作業系統中所提供的 ICU 庫的方式來實現,這種實現策略的代價是不同平臺的行為可能會存在一些(輕微)差異。
    • Android 端的支援是由微軟合作完成得。它幾乎涵蓋了從 ECMA-402 到 ES2020 的所有內容, 對體積的影響只有僅僅 3%(每個 ABI 約為 57-62K) 。我們在Twitter 上發起了投票,投票結果是強烈要求預設開啟 Intl ,因此,我們在 v0.8 中對其進行了預設支援。
    • Facebook 贊助了Major League Hacking 的一個遠端開源獎學金 專案。去年,我們和學生一起推出了Hermes 取樣分析器。今年,我們的學生將會和 Hermes,React Native 以及 Callstack 的成員一起,新增 Hermes 在 iOS 上的 Intl 的支援,敬請期待!
  • 我們很感謝各位幫助我們發現與解決那些影響社群使用問題的人。

總結

綜上所述,我們的願景是讓 Hermes 做好成為所有 React Native 平臺預設 JavaScript 引擎的準備,而且我們已經在朝這個方向努力了。我們希望充分聽取大家來自各方各面的反饋意見。

讓整個生態都能夠順利遷移對我們來說非常重要。我們鼓勵大家試用 Hermes,並根據情況在 GitHub 倉庫 中提交 issue,讓我們知道你的使用反饋、遇到哪些問題與不相容性、以及需要什麼功能等等。

致謝

衷心的感謝 Hermes 團隊、React Native 團隊以及 React Native 社群的眾多貢獻者們,感謝他們為改進 Hermes 做出的貢獻。

我個人還想特此感謝一下(按字母排序)Eli White,Luna Wei,Neil Dhar,Tim Yung,Tzvetan Mikov 以及其他同事在我寫作期間提供的幫助。

https://twitter.com/reactnative/status/1453067050411126787