🤔 移動端 JS 引擎哪家強?美國矽谷找……
- 2021 年 2 月 26 日
- 筆記
- react native, 前端, 前端工程化, 跨端開發

📌
如果你喜歡我寫的文章,可以把我的公眾號設為星標 🌟,這樣每次有更新就可以及時推送給你啦
在一般的移動端開發場景中,每次更新應用功能都是通過 Native 語言開發並通過應用市場版本分發來實現的。但是市場瞬息萬變,Native 語言在開發效率上存在一定不足,並且從 APP 版本更新
到 應用市場審核發布
再到 用戶下載更新
,總會存在一定的時間差,這樣就導致新的功能無法及時覆蓋全量用戶。
為了解決這個問題,開發者們一般會在項目里引入一門腳本語言,提速 APP 的研發流程。在移動端應用比較廣泛的腳本語言有 Lua 和 JavaScript,前者在遊戲領域用的比較多,後者在應用領域用的比較多。本篇文章主要是想探討一下移動雙端(iOS & Android)的 JavaScript 引擎選型。由於個人水平有限,文章總會有遺漏和不足的地方,還請各位大佬多多指教。
JS 引擎選型要點
JavaScript 作為世界上最熱門的腳本語言,有著非常多的引擎實現:有 Apple 御用的 JavaScriptCore,有性能最強勁的 V8,還有最近熱度很高的 QuickJS……如何從這些 JS 引擎里選出最適合的?我個人認為要有幾個考量:
-
性能:這個沒話說,肯定是越快越好 -
體積:JS 引擎會增加一定的包體積 -
記憶體佔用:記憶體佔用越少越好 -
JavaScript 語法支援程度:支援的新語法越多越好 -
調試的便捷性:是否直接支援 debug?還是需要自己編譯實現調試工具鏈 -
應用市場平台規範:主要是 iOS 平台,平台禁止應用集成帶 JIT 功能的虛擬機
比較麻煩的是,上面的幾個點都不是互相獨立的,比如說開啟 JIT 的 V8 引擎,性能肯定是最好的,但它引擎體積就很大,記憶體佔用也很高;在包體積上很佔優勢的 QuickJS,由於沒有 JIT 加持,和有 JIT 的引擎比起來平均會有 5-10 倍的性能差距。
下面我會綜合剛剛提到的幾個點,並選擇了 JavaScriptCore,V8,Hermes 和 QuickJS 這 4 個 JSVM,說說它們的優點和特點,再談談他們的不足。
JS 引擎功能大比拼
1.JavaScriptCore

JavaScriptCore 是 WebKit 默認的內嵌 JS 引擎,wikipedia 上都沒有獨立的詞條,只在 WebKit 詞條的三級目錄里介紹了一下,個人感覺還是有些不像話,畢竟也是老牌 JS 引擎了。
由於 WebKit 是 Apple 率先開源的,所以 WebKit 引擎運用在 Apple 自家的 Safari 瀏覽器和 WebView 上,尤其是 iOS 系統上,因為 Apple 的限制,所有的網頁只能用 WebKit 載入,所以 WebKit 在 iOS 上達到了事實壟斷,作為 WebKit 模組一部分的 JSC,順著政策春風,也「基本」壟斷了 iOS 平台的 JS 引擎份額。
壟斷歸壟斷,其實 JSC 的性能還是可以的,很多人不知道 JSC 的 JIT 功能其實比 V8 還要早,放在十幾年前是最好的 JS 引擎,只不過後來被 V8 追了上來。而且 JSC 有個重大利好,在 iOS7 之後,JSC 作為一個系統級的 Framework 開放給開發者使用,也就是說,如果你的 APP 使用 JSC,只需要在項目里 import
一下,包體積是 0 開銷的!這點在今天討論的 JS 引擎中,JSC 是最能打的。
雖然開啟 JIT 的 JSC 性能很好,但是只限於蘋果御用的 Safari 瀏覽器和 WKWebView,只有這兩個地方 JIT 功能才是默認開啟的,如果在項目里直接引入 JSC,JIT 功能是關閉的。為什麼這麼做呢?RednaxelaFX 大佬 給出過非常專業的解釋:
📌
JIT 編譯需要底層系統支援動態程式碼生成,對作業系統來說這意味著要支援動態分配帶有「可寫可執行」許可權的記憶體頁。當一個應用程式擁有請求分配可寫可執行記憶體頁的許可權時,它會比較容易受到攻擊從而允許任意程式碼動態生成並執行,這樣就讓惡意程式碼更容易有機可乘。
Apple 出於安全上的考慮,禁止了第三方 APP 使用 JSC 時開啟 JIT,這些特點在 React Native 的 JS Runtime 頁面也有過相關的解釋。不過在實際應用中,不做重 CPU 的運算只當膠水語言使用,JSC 還是綽綽有餘了。
上面的討論都是針對 iOS 系統的,在 Android 系統上,JSC 的表現就不盡人意了。JSC 並沒有對 Android 機型做很好的適配,雖然可以開啟 JIT,但是性能表現並不好,這也是 Facebook 決心製作 Hermes 的一個原因,具體的性能對比分析可見本文的 Hermes 小節。
最後再說說 JSC 的調試支援情況。如果是 iOS 平台,我們可以直接用 Safari 的 debbuger
功能調試,如果是 Android 平台,目前我還沒有找到一個很好的真機調試方法。
綜合來看,JavaScriptCore 在 iOS 平台上有非常明顯的主場優勢,各個指標都是很優秀的,但在 Android 上因為缺乏優化,表現並不是很好。
2.V8

V8,我想我不用過多解釋了,JavaScript 能有如今的地位,V8 功不可沒。性能沒得說,開啟 JIT 後就是業內最強(不止是 JS),有很多介紹 V8 的文章,我這裡就不多描述了,我們這裡說說 V8 在移動端的表現。
同樣作為 Google 家的產品,每一台 Android 手機上都安裝了基於 Chromium 的 WebView,V8 也一併捆綁了。但是 V8 和 Chromium 捆綁的太緊密了,不像 iOS 上的 JavaScriptCore 封裝為系統庫可以被所有 App 調用。這就導致你想在 Android 上用 V8 還得自己封裝,社區比較出名的項目是 J2V8,提供了 V8 的 Java bindings 案例。
V8 性能沒得說,Android 上可以開啟 JIT,但這些優勢都是有代價的:開啟 JIT 後記憶體佔用高,並且 V8 的包體積也不小(大概 7 MB
左右),如果作為只是畫 UI 的 Hybrid 系統,還是有些奢侈了。
我們再說說 V8 在 iOS 上的集成。V8 在 2019 年推出了 JIT-less V8,也就是關閉 JIT 只使用 Ignition interpreter
解釋執行 JS 文件,那麼我們在 iOS 上集成 V8 就成了可能,因為 Apple 還是支援接入只有解釋器功能的虛擬機引擎的。但是個人認為關閉了 JIT 的 V8 接入 iOS 價值不大,因為只開啟解釋器的話,這時候的 V8 和 JSC 的性能其實是差不多的,引入反而會增加一定的體積開銷。
V8 還有一個有意思的特性很少人提及,那就是——堆快照(Heap snapshots),這個是 V8 在 2015 年就支援的功能,但是社區里很少有人討論它。
堆快照是什麼原理呢?一般來說 JSVM 啟動後,第一步往往是解析 JS 文件,這個還是比較耗時的,V8 支援預先生成 Heap snapshots,然後直接載入到堆記憶體中,快速的獲得 JS 的初始化上下文。跨平台框架 NativeScript 就利用了這樣的技術,可以讓 JS 的載入速度提升 3 倍,技術細節可以看他們的博文。

V8 真機調試也需要引入第三方庫,Android 端社區上有人對 J2V8 做了 Chrome 調試協議的擴展,即 J2V8-Debugger 項目,iOS 我沒有找到相關的項目,可能需要自己實現一套擴展。
綜合來看 V8 的確是 JSVM 中的性能王者,Android 端使用時可以完全發揮它的威力,但是 iOS 平台因為主場劣勢,並不是很推薦。
3.Hermes

Hermes 是 FaceBook 2019 年中旬開源的一款 JS 引擎,從 release 記錄可以看出,這個是專為 React Native 打造的 JS 引擎,可以說從設計之初就是為 Hybrid UI 系統打造。
Hermes 一開始推出就是要替代原來 RN Android 端的 JS 引擎,即 JavaScriptCore(因為 JSC 在 Android 端表現太拉垮了)。我們可以理一下時間線,FaceBook 自從 2019-07-12 宣布 Hermes 開源後,jsc-android 的維護資訊就永遠的停在了 2019-06-25,這個訊號暗示得非常的明顯:JavaScriptCore Android 我們不再維護啦,大家都去用我們做的 Hermes 啊。
最近 Hermes 已經計劃伴隨 React Native 0.64 版本登錄 iOS 平台了,但是 RN 版本更新 blog 還沒有出,大家可以看看我之前對 Apple 開發者協議的解讀:Apple Agreement 3.3.2 規範解讀,在這裡我就不多說了。
Hermes 的特點主要是兩個,一個是不支援 JIT,一個是支援直接生成/載入位元組碼,我們在下面分開講一下。
Hermes 不支援 JIT 的主要原因有兩個:加入 JIT 後,JS 引擎啟動的預熱時間會變長,一定程度上會加長首屏 TTI(頁面首次載入可交互時間),現在的前端頁面都講究一個秒開,TTI 還是個挺重要的測量指標。另一個問題上 JIT 會增加包體積和記憶體佔用,Chrome 記憶體佔用高 V8 還是要承擔一定責任的。
因為不支援 JIT,Hermes 在一些 CPU 密集計算的領域就不佔優勢了,所以在 Hybrid 系統里,最優的解決方案就是充分發揮 JavaScript 膠水語言的作用,CPU 密集的計算(例如矩陣變換,參數加密等)放在 Native 里做,算好了再傳遞給 JS 表現在 UI 上,這樣可以兼顧性能和開發效率。
Hermes 最引人矚目的就是支援生成位元組碼了,我在之前的博文《🎯 跨端框架的核心技術到底是什麼?》也提到過,Hermes 加入 AOT 後,Babel
、Minify
、Parse
和 Compile
這些流程全部都在開發者電腦上完成,直接下發位元組碼讓 Hermes 運行就行,我們直接用個 demo 演示一下。

先寫個 test.js
的文件,裡面隨便寫點啥都行;然後編譯一下 Hermes 的源碼,編譯過程直接按文檔來就行,我這裡就略過了。
首先 Hermes 支援直接解釋運行 JS 程式碼,就是正常的 JS 載入編譯運行流程。
hermes test.js
我們可以加入 -emit-binary
參數嘗試一下生成 Bytecode 的功能:
hermes -emit-binary -out test.hbc test.js
然後就會生成一份 test.hbc
位元組碼文件:

最後我們可以讓 Hermes 直接載入運行 test.hbc
文件:
hermes test.hbc
客觀評價一下 Hermes 的位元組碼,首先省去了在 JS 引擎里解析編譯的流程,JS 程式碼的載入速度將會大大加快,體現在 UI 上就是 TTI 時間會明顯縮短;另一個優勢 Hermes 的位元組碼在設計時就考慮了移動端的性能限制,支援增量載入而不是全量載入,對記憶體受限的中低端 Android 機更友好;不過位元組碼的體積會比原來的 JS 文件會大一些,但是考慮到 Hermes 引擎本身體積就不大,綜合考慮下來這些體積增量還是可以接受的。
關於詳細的 Hermes 性能測試情況,網上有兩篇文章寫的比較好:一篇是 React Native Memory profiling: JSC vs V8 vs Hermes,可以看到在 Android 設備上 Hermes 的表現還是很優異的,而 JSC 的表現非常拉垮:

另一篇是攜程的文章:攜程對 RN 新一代 JS 引擎 Hermes 的調研,可以看出 Hermes 綜合成績最高(JSC 還是一樣的拉垮):

說完性能我們再說說 Hermes 的 JS 語法支援情況。Hermes 主要支援的是 ES6 語法,剛開源時不支援 Proxy
,不過 v0.7.0 已經支援了。他們的團隊也比較有想法,不支援 with
eval()
等這種屬於設計糟粕的 API,這種設計的權衡我個人還是比較認同的。
最後我們談談 Hermes 的調試功能。目前 Hermes 已經支援了 Chrome 的調試協議,我們可以直接用 Chrome 的 debugging 工具直接調試 Hermes 引擎,具體的操作可見文檔:Debugging JS on Hermes using Google Chrome’s DevTools
綜合來看,Hermes 是一款專為移動端 Hybrid UI System 打造的 JS 引擎,如果要自建一套 Hybrid 系統,Hermes 是一個非常好的選擇。
4.QuickJS

正式介紹 QuickJS 前我們先說說它的作者:Fabrice Bellard。
軟體界一直有個說法,一個高級程式設計師創造的價值可以超過 20 個平庸的程式設計師,但 Fabrice Bellard 不是高級程式設計師,他是天才,在我看來他的創造力可以超過 20 個高級程式設計師,我們可以順著時間軸理一下他創造過些什麼:
📌
1997年,發布了最快速的計算圓周率的演算法,此演算法是 Bailey-Borwein-Plouffe 公式的變體,前者的時間複雜度是O(n^3),他給優化成了O(n^2),使得計算速度提高了43%,這是他在數學上的成就 2000 年,發布了 FFmpeg,這是他在音影片領域的一個成就 2000,2001,2018 三年三度獲得國際混淆 C 程式碼大賽 2002 年,發布了TinyGL,這是他在圖形學領域的成就 2005 年,發布了 QEMU,這是他在虛擬化領域的成就 2011 年,他用 JavaScript 寫了一個 PC 虛擬機 Jslinux,一個跑在瀏覽器上的 Linux 作業系統 2019 年,發布了 QuickJS,一個支援 ES2020 規範的 JS 虛擬機
當人和人之間的差距差了幾個數量級後,羨慕嫉妒之類的情緒就會轉變為崇拜了,Bellard 就是一個這樣的人。
收復一下心情,我們來看一下 QuickJS 這個項目。QuickJS 繼承了 Fabrice Bellard 作品的一貫特色——小巧而又強大。
QuickJS 體積非常小,只有幾個 C 文件,沒有亂七八糟的第三方依賴。但是他的功能又非常完善,JS 語法支援到 ES2020,Test262 的測試顯示,QuickJS 的語法支援度比 V8 還要高。

那麼 QuickJS 的性能如何呢?QuickJS 官上有個基準測試,綜合比較了多款 JS 引擎對同一測試用例的跑分情況。下面是測試結果:

結合上面的表格和個人的一些測試,可以簡單的得出一些結論:
-
開啟 JIT 的 V8 綜合評分差不多是 QuickJS 的 35 倍,但是在同等主打輕量的 JS 引擎中,QuickJS 的性能還是很耀眼的 -
在記憶體佔用上,QuickJS 遠低於 V8,畢竟 JIT 是是吃記憶體的大戶,而且 QuickJS 的設計對嵌入式系統很友好(Bellard 成就獎盃 🏆 再 +1) -
QuickJS 和 Hermes 的跑分情況是差不多的,我私下做了一些性能測試,這兩個引擎的表現也很相近
因為 QuickJS 的設計,我不經好奇他和 Lua 的性能對比如何。Lua 是一門非常小巧精悍的語言,在遊戲領域和 C/C++ 開發中一直充當膠水語言的作用,我個人寫了一些測試用例,發現 QuickJS 和 Lua 的執行效率也是差不多的,後來在網上找到一篇博文 Lua vs QuickJS,這個老哥也做了一些測試,結論也是它倆的性能差不多,在部分場景 Lua 會比 QuickJS 快一些。
官方文檔里有提到,QuickJS 支援生成位元組碼,這樣可以免去 JS 文件編譯解析的過程。我一開始以為 QuickJS 和 Hermes 一樣,可以直接生成位元組碼,然後交給 QuickJS 解釋執行。後來自己編譯了一下才發現,QuickJS 的作用機制和 Hermes 還不太一樣:qjsc
生成位元組碼的 -e
和 -c
選項,都是先把 js 文件生成一份位元組碼,然後拼到一個 .c
文件里,大概長下面的這個樣子:
#include <quickjs/quickjs-libc.h>
const uint32_t qjsc_hello_size = 87;
// JS 文件編譯生成的位元組碼都在這個數組裡
const uint8_t qjsc_hello[87] = {
0x02, 0x04, 0x0e, 0x63, 0x6f, 0x6e, 0x73, 0x6f,
0x6c, 0x65, 0x06, 0x6c, 0x6f, 0x67, 0x16, 0x48,
0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x57, 0x6f, 0x72,
0x6c, 0x64, 0x22, 0x65, 0x78, 0x61, 0x6d, 0x70,
0x6c, 0x65, 0x73, 0x2f, 0x68, 0x65, 0x6c, 0x6c,
0x6f, 0x2e, 0x6a, 0x73, 0x0e, 0x00, 0x06, 0x00,
0x9e, 0x01, 0x00, 0x01, 0x00, 0x03, 0x00, 0x00,
0x14, 0x01, 0xa0, 0x01, 0x00, 0x00, 0x00, 0x39,
0xf1, 0x00, 0x00, 0x00, 0x43, 0xf2, 0x00, 0x00,
0x00, 0x04, 0xf3, 0x00, 0x00, 0x00, 0x24, 0x01,
0x00, 0xd1, 0x28, 0xe8, 0x03, 0x01, 0x00,
};
int main(int argc, char **argv)
{
JSRuntime *rt;
JSContext *ctx;
rt = JS_NewRuntime();
ctx = JS_NewContextRaw(rt);
JS_AddIntrinsicBaseObjects(ctx);
js_std_add_helpers(ctx, argc, argv);
js_std_eval_binary(ctx, qjsc_hello, qjsc_hello_size, 0);
js_std_loop(ctx);
JS_FreeContext(ctx);
JS_FreeRuntime(rt);
return 0;
}
因為這是個 .c
文件,想跑起來還得再編譯一次生成二進位文件。
從位元組碼這個設計點來看,QuickJS 和 Hermes 的定位還是不太一樣的。雖然直接生成位元組碼可以大大減少 JS 文本文件的解析時間,但是 QuickJS 還是更偏嵌入式一些,生成的位元組碼放在一個 C 文件中,還需要進行編譯才能運行;Hermes 為 React Native 而生,生成的位元組碼一開始就考慮到分發功能(熱更新就是一個應用場景),支援位元組碼的直接載入運行,不需要再編譯一次。
上面主要還是對性能的考量,下面我們看看開發體驗,首先是 QuickJS 的調試功能
支援。到目前為止(2021-02-22),QuickJS 還沒有官方的調試器,也就是說 debugger
語句會被忽略,社區有人實現了一套基於 VSCode 的調試器支援 vscode-quickjs-debug,但是會對 QuickJS 做一些訂製,個人還是蠻期待官方支援某個調試器協議的。
從 集成
的角度上看,社區上已經有了 iOS 和 Android 的示例項目,可以拿來用來參考接入到自己的工程中。
綜合來看,QuickJS 是一款潛力非常大的 JS 引擎,在 JS 語法高度支援的前提下,還把性能和體積都優化到了極致。在移動端的 Hybrid UI 架構和遊戲腳本系統都可以考慮接入。
選型思路
1.單引擎
單引擎的意思就是 iOS 端和 Android 端統一採用一個引擎,這樣做的話在 JS 層差異可以抹平,不容易出現同一份 JS 程式碼在 iOS 上運行是好的,Android 上就出錯的奇異 BUG。結合市面上的跨端方案,大概有下面三種選型:
-
統一採用 JSC:這個是 React Native 0.60 之前的方案 -
統一使用 Hermes:這個是 React Native 0.64 之後的設計方案 -
統一採用 QuickJS:QuickJS 體積很小,可以用來製作非常輕量的 Hybrid 系統
上面看出沒有統一採用 V8,這個就是我前面說的,V8 在 iOS 平台沒有主場優勢,關閉 JIT 後性能和 JSC 差不多,還會增大包體積,並不是很划算。
2.雙引擎
雙引擎也很好理解,就是 iOS 端和 Android 端各用各的,優點是可以發揮各自的主場優勢,缺點是可能會因為平台不一致導致雙端運行結果不統一,現在的方案有這麼幾種:
-
iOS 用 JSC,Android 用 V8:Weex,NativeScript 都是這樣的,可以在包體積和性能上有較好的均衡 -
iOS 用 JSC,Android 用 Hermes:React Natvie 現如今的方案 -
iOS 用 JSC,Android 用 QuickJS:滴滴的跨端框架 hummer 就是這樣的設計
從選型上看,iOS 上都選擇了 JSC,Android 各有各的選擇,倒是充分發揮了兩個平台的特色 : )
3.調試
無論是單引擎還是雙引擎,集成後的業務開發體驗也很重要。對於自帶 debugger 功能的引擎來說一切都不在話下,但是對於沒有實現調試協議的引擎來說,缺少 debugger 還是會影響體驗的。
但不是也沒有辦法,一般來說我們可以曲線救國,類似於 React Native 的 Remote JS Debugging
的思路,我們可以加個開關,把 JS 程式碼通過 websocket 傳送到 Chrome 的 Web Worker,然後用 Chrome 的 V8 進行調試。這樣做的優勢是可以調整一些業務上的 BUG,劣勢就是又會引入一個 JS 引擎,萬一遇到一些引擎實現的 BUG,就很難 debug 了。不過好在這種情況非常非常少見,我們也不能因噎廢食對吧。
總結
本文從性能、體積、調試便捷性等功能點出發,分析了 JavaScriptCore,V8,Hermes 和 QuickJS 這 4 款 JS 引擎,分別分析了它們的缺點和弱點,如果大家有移動端 JS 引擎選型的困惑,我認為從本文出發,還是可以給不少人以靈感的,希望我的這篇文章能幫助到大家。
參考鏈接
歡迎大家關注我的微信公眾號:滷蛋實驗室,目前專註前端技術,對圖形學也有一些微小研究。
也可以加我的微信 egg_labs,歡迎大家來撩。
