本文檔介紹了 V8 引擎是如何解析 JavaScript 源代碼的,以及咱們將改進它的計劃。 ui
咱們有個解析器和一個更快的預解析器(~2x),可是預解析器對大多數現代 JavaScript 無用。此外假如尚未編譯了外部函數,不然咱們必須再解析一遍內部函數。 那如今的 V8 引擎何時會當即編譯(eager compilation)呢?google
頂層的括號函數(function...spa
在 ( 以後的不是函數聲明的內部函數3d
當咱們把一個內部函數當作非腳本級的一部分來解析的時候,是不能使用預解析器的。咱們將不會編譯一個內部函數,因此若是咱們永遠不先行編譯函數,那麼通常的 n 層嵌套函數會先被預解析,而後會有 n 次解析(以及一次編譯)。或從另外一方面來看待它,考慮瞭如下格式的頂級「模塊」:code
!function f() { function A() { function B() {...} } function C() { function D() {...} } ... }(), function g() {....}()
壓縮程序常常用 (function() {...})();(function(){...}))() 替換上面的代碼。可是,你能夠看到它沒有被括號括起來,因此咱們不會在頂層運行完整的解析器。這是爲何呢?由於咱們不知道這些函數會被當即調用。所以會使用更快的預解析器。可是確實須要這些函數,這就打臉了,而 V8 就不得不去再解析。預解析階段會將整個頂層函數看一遍,包括 A, B, C, D(假如本來的 (function f() {...})() 編譯過那麼就會跳過本階段)。 blog
由於頂層函數被調用了,因此引擎會去解析它。此次解析是徹底的。爲何呢?由於須要作範圍解析以便知道在哪裏分配變量。而惟一的正確的方式是知道什麼變量被引用了。而知道什麼變量被引用的惟一方式是對內部函數也進行徹底的解析。因此解析/編譯頂層函數會迫使引擎對 A,B,C,D 這些函數也進行徹底解析。ip
如今咱們須要調用函數 A,所以須要去編譯它。爲了解析它,咱們也須要知道在哪裏分配變量。就像我說的:你須要知道從內部函數引用了上面。因此咱們徹底解析了 B 函數。內存
如今咱們假定預解析須要1費,解析須要2費。編譯它須要另外的2費,可是咱們實際上編譯的是壓縮的版本,所以能夠忽略。 假如如今運行 A 函數,那麼將花費 3*(f + A + B) + 2*(A + B)。若是 A 將會調用 B,咱們就花費另外的2費用於 B 函數。v8
一方面,要獲得一個內部函數,你須要解析一大堆。 另外一方面,頂層函數越多,解析它的成本就越高,由於你要算上解析全部嵌套函數的時間。
那麼計劃怎麼解決呢?
預解析的同時也進行範圍解析(scope resolution),這樣未編譯部分的花費會從2下降到1.x。
將函數的上下文內存分配信息序列化到持久存儲中,以免沒必要要的重解析成本。
當即編譯可能會支持 ! 和 , 。
至於成本?全部懶解析函數在初始加載時的開銷爲1.x,若是實際使用則爲3.x。.x是內部函數範圍解析和序列化的額外未知成本。
當即編譯的優勢是,對於已知的當即執行的頂級函數,咱們能夠進一步將成本從3.x降低到2。它是從頂層向內的。若是咱們決定不將預解析(eagerly parse)做爲主編譯工做的一部分,那麼咱們能夠等到它被執行,這樣咱們至少能夠肯定只須要支付實際使用的功能的編譯成本(2解析和2編譯)。
當即編譯的缺點是咱們須要在解析和編譯之間在內存中保持AST(Abstract syntax tree)。顯著增長了使用內存的峯值。若是咱們能夠預解析那些在被當即解析的函數的內部函數,狀況可能會看起來好多了。即便如此,在低內存設備上,咱們應該禁用啓發式的當即編譯。
若是咱們序列化這些數據,那麼在熱啓動時就完徹底全不須要去查看未使用的代碼。熱啓動時,即便在頂層的時候也是與啓發式的當即編譯不相關,由於咱們將只解析/編譯咱們須要的函數。 搭載了比使用的代碼還要多10倍以上的頁面將會當即熱啓動。(目前已經能夠是這種狀況,但它是一個有點hit-and-miss)。
相關連接: