朝向 Hermes 成為預設
自從 我們在 2019 年公告 Hermes 以來,它在社群中的採用率不斷提高。Expo 團隊維護 React Native 應用程式的熱門 meta 框架,最近公告實驗性 支援 Hermes,因為 Hermes 是 Expo 最受歡迎的功能之一。Realm 團隊是熱門的行動資料庫,最近也發佈其 Hermes 的 Alpha 支援。在這篇文章中,我們想強調過去兩年我們在推動 Hermes 成為 React Native 最佳 JavaScript 引擎方面所取得的一些最令人興奮的進展。展望未來,我們有信心,透過這些改進和更多即將到來的改進,我們可以讓 Hermes 成為所有平台上 React Native 的預設 JavaScript 引擎。
針對 React Native 進行最佳化
Hermes 的定義功能是它如何提前執行編譯工作,這表示啟用 Hermes 的 React Native 應用程式隨附預先編譯的最佳化位元組碼,而不是純 JavaScript 原始碼。這大幅減少了使用者啟動產品所需的工作量。來自 Facebook 和社群應用程式的測量結果表明,啟用 Hermes 通常會將產品的 TTI(或 Time-To-Interactive)指標縮短近一半。
話雖如此,我們一直在努力改進 Hermes 的許多其他方面,使其成為更出色的 JavaScript 引擎,專門用於 React Native。
為 Fabric 建置全新垃圾收集器
隨著即將推出的全新 React Native 架構中的 Fabric 渲染器,可以在 UI 執行緒上同步呼叫 JavaScript。但是,這表示如果 JavaScript 執行緒執行時間過長,可能會導致明顯的 UI 畫面丟失並封鎖使用者輸入。並行渲染 由 React Fiber 啟用,將渲染工作分割成區塊,避免排程長時間執行的 JavaScript 工作。但是,JavaScript 執行緒延遲的另一個常見來源是 — 當 JavaScript 引擎必須「停止世界」以執行垃圾收集 (GC) 時。
Hermes 中先前的預設垃圾收集器 GenGC 是一個單執行緒分代垃圾收集器。新世代使用典型的半空間複製策略,而舊世代使用標記壓縮策略,使其非常擅長積極地將記憶體返回給作業系統。由於其單執行緒,GenGC 具有導致長時間 GC 暫停的缺點。在像 Facebook for Android 這樣複雜的應用程式上,我們觀察到平均暫停時間為 200 毫秒,或 p99 時為 1.4 秒。考慮到 Facebook for Android 龐大且多樣化的使用者群,我們甚至看到它長達 7 秒。
為了減輕這個問題,我們實作了一個全新的主要並行 GC,名為 Hades。Hades 以與 GenGC 完全相同的方式收集其年輕世代,但它使用開始時快照樣式標記掃描收集器來管理其舊世代。這可以透過在背景執行緒中執行其大部分工作,而不會阻止引擎的主要執行緒執行 JavaScript 程式碼,從而顯著減少 GC 暫停時間。我們的統計資料顯示,在 64 位元裝置上,Hades 在 p99.9 時僅暫停 48 毫秒(比 GenGC 快 34 倍!),在 32 位元裝置上約為 88 毫秒(在 32 位元裝置上,它作為單執行緒增量 GC 運作)。這些暫停時間改進可能會以整體輸送量為代價,因為需要更昂貴的寫入屏障、較慢的基於可用空間列表的配置(而不是 bump 指標配置器)以及增加的堆積碎片。我們認為這些是正確的權衡,並且我們能夠透過合併和我們將討論的其他記憶體最佳化來實現整體更低的記憶體消耗。
打擊效能痛點
應用程式的啟動時間對於許多應用程式的成功至關重要,我們不斷推進 React Native 的界限。對於我們在 Hermes 中實作的任何新 JavaScript 功能,我們都會仔細監控它們對生產效能的影響,並確保它們不會降低指標。在 Facebook,我們目前正在實驗 Metro 中 Hermes 的專用 Babel 轉換設定檔,以使用 Hermes 的原生 ESNext 實作取代十幾個 Babel 轉換。我們能夠在許多介面上觀察到 18-25% 的 TTI 改善和整體位元組碼大小減少,並且我們期望在 OSS 上看到類似的結果。
除了啟動效能外,我們還將記憶體佔用空間確定為 React Native 應用程式的改進機會,特別是對於 虛擬實境。 由於我們作為 JavaScript 引擎擁有的低階控制,我們能夠透過擠壓位元和位元組來提供多輪記憶體最佳化
- 先前,所有 JavaScript 值都表示為 64 位元 NaN-boxing 編碼標記值,以表示 64 位元架構上的浮點雙精度值和指標。但是,這在實務上是浪費的,因為大多數數字都是小整數 (SMI),並且用戶端應用程式的 JavaScript 堆積通常預期不會大於 4GiB。為了解決這個問題,我們引入了一種新的 32 位元編碼,其中 SMI 和指標編碼為 29 位元(因為指標是 8 位元組對齊的,我們可以假設底部 3 個位元始終為零),而其餘的 JS 數字則框到堆積上。這減少了約 30% 的 JavaScript 堆積大小。
- 不同類型的 JavaScript 物件在 JavaScript 堆積中表示為不同類型的 GC 管理儲存格。透過積極最佳化這些儲存格標頭的記憶體版面配置,我們能夠將記憶體使用量再減少約 15%。
我們對 Hermes 的主要決策之一是不實作 即時 (JIT) 編譯器,因為我們認為對於大多數 React Native 應用程式來說,額外的預熱成本以及二進位檔和記憶體上的額外佔用空間實際上並不值得。多年來,我們投入大量精力來最佳化直譯器效能和編譯器最佳化,以使 Hermes 的輸送量在 React Native 工作負載方面與其他引擎競爭。我們將繼續專注於透過識別來自各處(直譯器分派迴圈、堆疊版面配置、物件模型、GC 等)的效能瓶頸來提高輸送量。期待在即將發佈的版本中看到更多數字!
垂直整合的先驅
在 Facebook,我們傾向於將專案並置在大型 monorepo 中。透過讓引擎 (Hermes) 和主機 (React Native) 緊密地迭代在一起,我們為垂直整合開闢了很大的空間。列舉幾個例子
- Hermes 透過使用 Chrome DevTools Protocol,支援 使用 Chrome 偵錯工具在裝置上進行 JavaScript 偵錯。它比舊版「遠端 JS 偵錯」(它使用應用程式內代理在桌面版 Chrome 中執行 JS)更好,因為它支援偵錯同步原生呼叫並保證一致的執行階段環境。結合 React DevTools、Metro、Inspector 等,Hermes 偵錯工具現在是 Flipper 的一部分,以提供一站式開發人員體驗。
- 在 React Native 應用程式的初始化路徑期間配置的物件通常是長期存在的,並且不遵循分代 GC 利用的分代假設。因此,我們在 React Native 中配置 Hermes,將前 32MiB 直接配置到舊世代(稱為預先老化),以避免觸發 GC 暫停並延遲 TTI。
- 全新 React Native 架構主要基於 JSI(或 JavaScript Interface),這是一個輕量級、通用 API,用於將 JavaScript 引擎嵌入到 C++ 程式中。透過讓維護 JS 引擎的團隊也維護 JSI API 實作,我們有信心提供最佳的整合,該整合在 Facebook 的規模下是可靠、高效且經過實戰測試的。
- 取得 JavaScript 並行基本元素(例如 promise)和平台並行基本元素(例如 microtask)在語義上正確且高效對於 React 並行渲染和 React Native 應用程式的未來至關重要。從歷史上看,React Native 中的 promise 是使用非標準化
setImmediate
API polyfill 的。我們正在努力使來自 JS 引擎的原生 promise 和 microtask 透過 JSI 提供,並將queueMicrotask
(最近添加到 Web 標準中的功能)引入平台,以更好地支援現代非同步 JavaScript 程式碼。
攜手整個社群
Hermes 對於 Facebook 來說非常棒。但是在我們的社群可以使用 Hermes 來支援整個生態系統的體驗之前,我們的工作尚未完成,以便每個人都能利用其所有功能並擁抱其全部潛力。
擴展到新平台
Hermes 最初僅針對 Android 上的 React Native 開放原始碼。從那時起,我們很高興看到我們的社群成員將 Hermes 支援擴展到 React Native 生態系統已擴展的許多其他平台。
Callstack 領導了將 Hermes 引入 React Native 0.64 中的 iOS 的工作。他們撰寫了 一系列文章 並主持了一個 podcast,說明他們如何實現這一目標。根據他們的基準測試,與 Mattermost 應用程式的 JSC 相比,Hermes 能夠持續提供約 40% 的啟動改善和約 18% 的記憶體減少,且僅增加 2.4 MiB 的應用程式大小額外負荷。我鼓勵您親眼看看實際情況。
Microsoft 一直在將 Hermes 引入 React Native for Windows 和 macOS。在 Microsoft Build 2020 上,Microsoft 分享了 Hermes 的記憶體影響(工作集)比 React Native for Windows 上的 Chakra 引擎低 13%。最近,在一些綜合基準測試中,他們發現 Hermes 0.8(隨附 Hades 以及上述 SMI 和指標壓縮最佳化)比其他引擎使用的記憶體少 30%-40%。毫不奇怪,建置在 React Native 上的 桌面版 Messenger 視訊通話體驗也由 Hermes 提供支援。
最後但並非最不重要的一點是,Hermes 也一直在為 Oculus 上使用 React 系列技術建置的所有虛擬實境體驗提供支援,包括 Oculus Home。
支援我們的社群
我們承認仍然存在一些障礙阻止社群的部分成員採用 Hermes,我們致力於為這些遺失的功能建置支援。我們的目標是功能齊全,以便 Hermes 成為大多數 React Native 應用程式的正確選擇。以下是社群如何塑造 Hermes 藍圖
Proxy
和Reflect
最初從 Hermes 中排除,因為 Facebook 不使用它們。我們也擔心即使不使用 Proxy,新增 Proxy 也會損害屬性查閱效能。但是,由於 MobX 和 Immer 等熱門程式庫,Proxy 很快成為 Hermes 最受歡迎的功能。我們仔細評估並決定僅為社群建置它,並且我們設法以非常低的成本實作它。由於這是我們不使用的功能,因此我們依賴社群來證明其穩定性。我們首先在標記後面測試 Proxy,並為 版本 v0.4 和 v0.5 建立選擇加入 npm 套件,並且從 v0.7 開始預設啟用。- ECMAScript Internationalization API Specification (ECMA-402 或
Intl
) 是第二個最受歡迎的功能。Intl
是一組龐大的 API,通常需要實作才能包含 6MB 價值的 Unicode CLDR 資料。這就是為什麼像 FormatJS (又名react-intl
) 和 JS 引擎(如 社群 JSC 的國際變體組建)這樣的 polyfill 如此龐大。為了避免大幅增加 Hermes 的二進位檔大小,我們決定透過消耗和對應作業系統中包含的程式庫提供的 ICU 功能,以另一種策略來實作它,但代價是跨平台行為存在一些(通常很小的)差異。- Microsoft 協作在 Android 上建置支援。它幾乎涵蓋了從 ECMA-402 到 ES2020 的所有內容,大小影響僅小至 3%(每個 ABI 57-62K)。我們在 Twitter 上進行了一項投票,結果強烈贊成預設包含
Intl
,因此我們這樣做了,並且從 版本 v0.8 開始提供。 - Facebook 已贊助 Major League Hacking 啟動 遠端開放原始碼獎學金計畫。去年,我們推出了 Hermes 採樣分析器。今年,我們的研究員將與來自 Hermes、React Native 和 Callstack 的成員合作,在 iOS 上新增對 Hermes
Intl
的支援。敬請期待!
- Microsoft 協作在 Android 上建置支援。它幾乎涵蓋了從 ECMA-402 到 ES2020 的所有內容,大小影響僅小至 3%(每個 ABI 57-62K)。我們在 Twitter 上進行了一項投票,結果強烈贊成預設包含
- 我們感謝人們一直與我們合作,發現影響社群的問題。
- 人們幫助我們確定了關鍵規格分歧,例如
Array.prototype.sort
的穩定性,該穩定性在 ES2019 中修訂。這已修復,將在下一個版本中提供。 - 人們發現我們的預設堆積大小限制太小,並為許多不熟悉自訂 Hermes GC 配置的使用者造成 不必要的 GC 壓力 和 OOM 崩潰。因此,我們將其從 512MiB 增加到 3GiB,使其在預設情況下對大多數使用者來說都綽綽有餘。
- 人們也回報說,我們專門的
Function.prototype.toString
實作 導致在執行不正確功能偵測的程式庫中效能下降,並且 阻止使用者執行原始碼注入。這幫助我們加強了我們的立場,即 Hermes 應盡可能不阻礙開發人員,並尊重事實上的做法。
- 人們幫助我們確定了關鍵規格分歧,例如
摘要
總之,我們的願景是讓 Hermes 準備好成為所有 React Native 平台上的預設 JavaScript 引擎。我們已經開始朝著這個方向努力,我們想聽取大家對這個方向的看法。
為生態系統做好順利採用的準備對我們來說至關重要。我們鼓勵您試用 Hermes,並在我們的 GitHub 儲存庫 上提交問題,以取得任何意見反應、問題、功能要求和不相容性。
感謝
我們衷心感謝 Hermes 團隊、React Native 團隊以及 React Native 社群的許多貢獻者為改進 Hermes 所做的工作。
我也想親自感謝(按字母順序排列)Eli White、Luna Wei、Neil Dhar、Tim Yung、Tzvetan Mikov 和許多其他人在寫作期間提供的幫助。