合理使用IIFE優化JS引擎的效能

發表於2016-11-16

v2-e8c0580b5fb373973e0536117a617341_b

說起立即執行函式(IIFE,Immediately-invoked function expression)大家應該都不陌生,在 JavaScript 中可以宣告一個函式然後立即執行它:

IIFE 通常用於實現私有變數、實現獨立模組等等地方,比如喜聞樂見的 jQuery 最頂層的結構就是這樣的:

但我們今天要說的不是 IIFE 怎麼用,而是關於它針對 JS 引擎的一處效能優化。

先從一個小問題說起吧,想實現一個立即執行的函式,我們有很多種寫法,比如下面這兩種:

是的,這兩種寫法完全是等價的,無論怎麼看都不會有什麼區別,但是在一些 JavaScript 引擎中,它們其實效能相差甚遠。

我使用 Node 分別對兩種情況執行了十萬次,方法一的執行時間平均在360毫秒左右,方法二平均是 50 毫秒左右,效能相差7倍還多。

為什麼使用 IIFE 之後效能會提升那麼多呢?這就要從 JS 引擎(比如V8、SpiderMonkey)對於函式的優化上說起了。

現在的 JS 引擎都是十分聰明的,它們在真正執行程式碼之前會對程式碼中函式宣告做一遍 pre-parse(預解析),為啥要做 pre-parse 呢?因為實際情況中大多數的函式都不是立即被使用的(甚至完全沒被呼叫過),不需要對它們做一次完整的解析,只需要做一次效能開銷更小的 pre-parse(比如檢查一下語法錯誤),等函式真正被呼叫時,再進行完整的 full-parse。

但是有個問題!對於立即執行函式這種奇葩來說,它不適用於上面的規則,應該直接進行 full-parse。現在的大多數引擎也完全考慮到了這一點:

但是還有個問題!!現在的大多數引擎檢測 IIFE 的時候都不完全,大部分都是通過判別函式宣告前有沒有類似『 ( 』或者『 ! 』這樣的字元來實現的,比如下面這種情況就被忽略掉了:

所以我們可以通過一個小 trick 來優化這裡的效能(加了一對括號),這樣引擎就會把這裡識別為立即執行函式,然後只做一次 full-parse:

所以針對這個問題,有一個專門的小工具來解決:

nolanlawson/optimize-js: Optimize a JavaScript file for faster initial load by wrapping eagerly-invoked functions

還有一個相關的討論:

Turn off negate_iife by default as it hurts V8 performance. · Issue #886 · mishoo/UglifyJS2

這個看似不起眼的 trick 實際對於效能有很顯著的提升:

v2-1550a61ad1198349e328131e04981f21_b

這個問題本質上來講是 JS 引擎對於立即執行函式的識別有遺漏導致的,比如在 Safari 10 中這個問題基本不會發生,而 Chrome 的 V8 中就經常出現。不過感覺隨著引擎版本的迭代,這個問題應該會得到修復。

相關文章