溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

V8是怎么快速地解析JavaScript延遲解析的

發(fā)布時(shí)間:2021-11-06 14:58:00 來(lái)源:億速云 閱讀:195 作者:iii 欄目:web開(kāi)發(fā)

本篇內(nèi)容介紹了“V8是怎么快速地解析JavaScript延遲解析的”的有關(guān)知識(shí),在實(shí)際案例的操作過(guò)程中,不少人都會(huì)遇到這樣的困境,接下來(lái)就讓小編帶領(lǐng)大家學(xué)習(xí)一下如何處理這些情況吧!希望大家仔細(xì)閱讀,能夠?qū)W有所成!

解析是將源代碼轉(zhuǎn)換成一個(gè)中間表示形式供編譯器使用的步驟(在V8中,是字節(jié)碼編譯器Ignition)。解析和編譯發(fā)生在web頁(yè)面啟動(dòng)的關(guān)鍵路徑上,在啟動(dòng)期間,并不是所有提供給瀏覽器的函數(shù)都需要被調(diào)用。盡管開(kāi)發(fā)人員可以使用異步和延遲腳本來(lái)延遲這些代碼的加載,但這并不總是可行的。此外,許多web頁(yè)面的代碼只能被特定的特性使用,這樣一來(lái),在每個(gè)頁(yè)面單獨(dú)運(yùn)行期間,用戶是根本無(wú)法訪問(wèn)這些代碼的。

急切地編譯不必要的代碼會(huì)產(chǎn)生實(shí)際的資源成本:

  • 創(chuàng)建這些不必要的代碼會(huì)占用CPU的一部分時(shí)間,這會(huì)導(dǎo)致啟動(dòng)時(shí)實(shí)際需要的代碼延遲加載。

  • 代碼對(duì)象會(huì)占用內(nèi)存,至少在回收機(jī)制判定當(dāng)前代碼不再需要并允許垃圾收集器回收之前是這樣的。

  • ***腳本結(jié)束執(zhí)行時(shí)編譯的代碼最終會(huì)緩存在磁盤上,占用磁盤空間。

由于這些原因,所有主流瀏覽器都實(shí)現(xiàn)了延遲解析。以前的做法是為每個(gè)函數(shù)生成一個(gè)抽象語(yǔ)法樹(shù)(AST),然后將其編譯為字節(jié)碼,而使用了延遲解析之后,解析器就可以“預(yù)解析”它遇到的函數(shù),而不需要對(duì)這些函數(shù)進(jìn)行完全解析。它通過(guò)切換到預(yù)解析器來(lái)實(shí)現(xiàn)這一點(diǎn),而預(yù)解析器是解析器的一個(gè)副本,它只做最基本的工作,否則就會(huì)跳過(guò)該函數(shù)。預(yù)解析器驗(yàn)證它跳過(guò)的函數(shù)在語(yǔ)法上是否是有效的,并生成正確編譯外部函數(shù)所需的所有信息。在后邊調(diào)用預(yù)解析的函數(shù)時(shí),將按需對(duì)其進(jìn)行完全解析和編譯。

變量分配

使預(yù)解析復(fù)雜化的主要問(wèn)題是變量分配。

出于性能原因,函數(shù)激活是在機(jī)器堆棧上進(jìn)行管理的。例如,如果函數(shù)g調(diào)用了參數(shù)為1和2的函數(shù)f:

V8是怎么快速地解析JavaScript延遲解析的

首先將接收器(即f的this值,由于它是一個(gè)草率的函數(shù)調(diào)用,所以它是globalThis)推入堆棧,接著是被調(diào)用的函數(shù)f。然后再將參數(shù)1和2推入堆棧。此時(shí)函數(shù)f被調(diào)用。為了執(zhí)行調(diào)用,我們首先將g的狀態(tài)保存在堆棧上:  包括f的“返回指令指針”(rip;我們需要返回什么代碼)以及“幀指針”(fp;返回時(shí)堆棧應(yīng)該是什么樣子的)。然后我們輸入f,它為局部變量c分配空間,以及它可能需要的任何臨時(shí)空間。這確保了當(dāng)函數(shù)激活超出作用域時(shí),函數(shù)使用的任何數(shù)據(jù)都會(huì)消失:  它只是從堆棧中彈出。

V8是怎么快速地解析JavaScript延遲解析的

對(duì)帶有參數(shù)a,b和局部變量c的函數(shù)f的調(diào)用的堆棧分配布局。

這種設(shè)置的問(wèn)題是函數(shù)可以引用在外部函數(shù)中聲明的變量。內(nèi)部函數(shù)存活的時(shí)間可能會(huì)比它們被創(chuàng)建時(shí)的激活時(shí)間要長(zhǎng):

V8是怎么快速地解析JavaScript延遲解析的

在上面的例子中,從inner到make_f中聲明的變量d的引用會(huì)在make_f返回后進(jìn)行計(jì)算。為了實(shí)現(xiàn)這一點(diǎn),使用詞法閉包的語(yǔ)言的虛擬機(jī)會(huì)在一個(gè)稱為“上下文”的結(jié)構(gòu)中分配從堆上的內(nèi)部函數(shù)中引用的變量。

V8是怎么快速地解析JavaScript延遲解析的

通過(guò)將make_f的參數(shù)復(fù)制到一個(gè)上下文中來(lái)對(duì)它進(jìn)行調(diào)用,該調(diào)用的堆棧布局會(huì)在堆上進(jìn)行分配,供捕捉d的inner稍后使用。

這意味著對(duì)于函數(shù)中聲明的每個(gè)變量,我們需要知道內(nèi)部函數(shù)是否引用了該變量,以便決定是在棧上分配該變量,還是在堆上分配的上下文中分配該變量。當(dāng)我們計(jì)算一個(gè)函數(shù)的字面量時(shí),我們分配一個(gè)閉包,它指向函數(shù)的代碼和當(dāng)前上下文:  包含函數(shù)可能需要訪問(wèn)的變量值的對(duì)象。

長(zhǎng)話短說(shuō),我們至少需要跟蹤預(yù)解析器中的變量引用。

如果我們只跟蹤引用,就會(huì)過(guò)多估計(jì)引用的變量。在外部函數(shù)中聲明的變量可以通過(guò)內(nèi)部函數(shù)中的重新聲明來(lái)隱藏,從而創(chuàng)建一個(gè)來(lái)自該內(nèi)部函數(shù)的引用,并將其指向內(nèi)部聲明,而不是外部聲明。如果我們無(wú)條件地在上下文中分配外部變量,程序性能就會(huì)受到影響。因此,要使變量分配能正確地處理預(yù)解析過(guò)程,我們需要確保預(yù)解析后的函數(shù)正確地跟蹤變量引用和聲明。

頂層代碼是這條規(guī)則的一個(gè)例外。一個(gè)腳本的頂層總是堆分配的,因?yàn)樽兞吭谀_本之間是可見(jiàn)的。接近良好工作的體系結(jié)構(gòu)的一個(gè)簡(jiǎn)單方法是簡(jiǎn)單地運(yùn)行預(yù)解析器,而不需要對(duì)快速解析的頂層函數(shù)進(jìn)行變量跟蹤;并為內(nèi)部函數(shù)使用完整的解析器,但在編譯的時(shí)候跳過(guò)它們。這比預(yù)解析過(guò)程成本更高,因?yàn)槲覀儾恍枰獦?gòu)建整個(gè)AST,但它使我們啟動(dòng)并運(yùn)行。這正是V8在新版本V8  v6.3 / Chrome 63中所做的。

向預(yù)解析器說(shuō)明變量的情況

跟蹤預(yù)解析器中的變量聲明和引用是非常復(fù)雜的,因?yàn)樵贘avaScript中,某些部分表達(dá)式的含義從一開(kāi)始就不清楚。例如,假設(shè)我們有一個(gè)帶參數(shù)d的函數(shù)f,它有一個(gè)內(nèi)部函數(shù)g,從表達(dá)式看起來(lái)g可能引用了d。

V8是怎么快速地解析JavaScript延遲解析的

它最終可能確實(shí)會(huì)引用d,因?yàn)槲覀兛吹降膖okens標(biāo)記是析構(gòu)賦值表達(dá)式的一部分。

V8是怎么快速地解析JavaScript延遲解析的

它最終也可能是一個(gè)帶有析構(gòu)參數(shù)d的箭頭函數(shù),在這種情況下,f中的d就沒(méi)有被g引用。

V8是怎么快速地解析JavaScript延遲解析的

最初,我們的預(yù)解析器是作為解析器的獨(dú)立副本實(shí)現(xiàn)的,沒(méi)有太多的共享,這導(dǎo)致兩個(gè)解析器會(huì)隨著時(shí)間的推移而產(chǎn)生分歧。通過(guò)將解析器和預(yù)解析器重寫為基于實(shí)現(xiàn)了奇異遞歸模板模式的ParserBase,我們成功地***化了共享,同時(shí)也保留了單獨(dú)副本的性能優(yōu)勢(shì)。這大大簡(jiǎn)化了向預(yù)解析器添加全部變量跟蹤的工作,因?yàn)檫@個(gè)實(shí)現(xiàn)的大部分內(nèi)容可以在解析器和預(yù)解析器之間共享。

實(shí)際上,忽略變量聲明和頂層函數(shù)的引用是不正確的。ECMAScript規(guī)范要求在***次解析腳本時(shí)要檢測(cè)各種類型的變量沖突。例如,如果一個(gè)變量在同一作用域內(nèi)被兩次聲明為詞法變量,則被認(rèn)為是early  SyntaxError。因?yàn)槲覀兊念A(yù)解析器只是跳過(guò)了變量聲明,所以在預(yù)解析過(guò)程中它將允許代碼錯(cuò)誤地運(yùn)行。此時(shí)我們認(rèn)為性能上的勝利使對(duì)規(guī)范的違反情有可原?,F(xiàn)在預(yù)解析器  能正確地跟蹤變量,盡管如此,我們還是應(yīng)該在沒(méi)有明顯性能代價(jià)的情況下消除這類與變量解析相關(guān)的違反規(guī)范的行為。

跳過(guò)內(nèi)部函數(shù)

如前所述,當(dāng)***次調(diào)用一個(gè)預(yù)解析的函數(shù)時(shí),我們將對(duì)其進(jìn)行完全解析,并將生成的AST編譯為字節(jié)碼。

V8是怎么快速地解析JavaScript延遲解析的

該函數(shù)直接指向外部上下文,其中包含內(nèi)部函數(shù)需要使用的變量聲明的值。為了允許函數(shù)的延遲編譯(并支持調(diào)試器),上下文會(huì)指向一個(gè)名為ScopeInfo的元數(shù)據(jù)對(duì)象。ScopeInfo對(duì)象描述了上下文中列出的變量。這意味著在編譯內(nèi)部函數(shù)時(shí),我們可以計(jì)算變量在上下文鏈中的位置。

但是,要計(jì)算延遲編譯的函數(shù)本身是否需要上下文,我們需要再次執(zhí)行范圍解析:  我們需要知道嵌套在延遲編譯的函數(shù)中的函數(shù)是否引用了由延遲函數(shù)聲明的變量。我們可以通過(guò)重新解析這些函數(shù)來(lái)計(jì)算出來(lái)。這正是V8在升級(jí)到V8v6.3/Chrome63之前所做的。但是,這并不是理想的性能***的方法,因?yàn)樗官Y源大小和解析成本之間的關(guān)系變成非線性:  我們將盡可能多地解析嵌套函數(shù)。除了動(dòng)態(tài)程序的自然嵌套之外,JavaScript打包器通常用“即時(shí)調(diào)用函數(shù)表達(dá)式”(IIFEs)的方式來(lái)包裝代碼,這使得大多數(shù)JavaScript程序具有多個(gè)嵌套層。

V8是怎么快速地解析JavaScript延遲解析的

每次重新解析至少會(huì)增加解析函數(shù)的成本。

為了避免非線性性能開(kāi)銷,我們甚至在預(yù)解析過(guò)程中執(zhí)行全作用域解析。我們存儲(chǔ)了足夠的元數(shù)據(jù),這樣我們稍后就可以簡(jiǎn)單地跳過(guò)內(nèi)部函數(shù),而不必重新解析它們。一種方法是存儲(chǔ)由內(nèi)部函數(shù)引用的變量名。這樣做的存儲(chǔ)成本很高,并要求我們?nèi)匀贿M(jìn)行重復(fù)工作:我們已經(jīng)在預(yù)解析期間執(zhí)行了變量解析。

相反,我們將在變量分配的地方將每一個(gè)變量序列化為它的一個(gè)密集標(biāo)記數(shù)組。當(dāng)我們延遲解析一個(gè)函數(shù)時(shí),變量按照預(yù)解析器看到的順序被重新創(chuàng)建,我們可以簡(jiǎn)單地將元數(shù)據(jù)應(yīng)用于這些變量。現(xiàn)在函數(shù)已經(jīng)編譯完成,已經(jīng)不再需要變量分配元數(shù)據(jù)了,這樣它就可以被當(dāng)做垃圾進(jìn)行回收。由于我們只需要這個(gè)元數(shù)據(jù)來(lái)處理實(shí)際包含內(nèi)部函數(shù)的函數(shù),所以大部分函數(shù)甚至不需要這個(gè)元數(shù)據(jù),從而顯著地降低了內(nèi)存開(kāi)銷。

V8是怎么快速地解析JavaScript延遲解析的

通過(guò)跟蹤預(yù)解析的函數(shù)的元數(shù)據(jù),我們可以完全跳過(guò)內(nèi)部函數(shù)。

跳過(guò)內(nèi)部函數(shù)的性能影響是非線性的,就像重新預(yù)解析內(nèi)部函數(shù)的開(kāi)銷一樣。有些站點(diǎn)將它們的所有函數(shù)都提升到了頂層范圍。因?yàn)樗鼈兊那短讓訑?shù)總是0,所以開(kāi)銷也總是0。然而,許多現(xiàn)代的站點(diǎn)實(shí)際上都有許多深層嵌套函數(shù)。當(dāng)V8  v6.3 / Chrome 63啟動(dòng)該特性時(shí),我們就會(huì)在這些站點(diǎn)上看到顯著的改進(jìn)。啟用該特性的主要優(yōu)點(diǎn)是,現(xiàn)在代碼的嵌套深度已經(jīng)無(wú)關(guān)緊要:  任何函數(shù)最多只預(yù)解析一次,完全解析一次[1]。

V8是怎么快速地解析JavaScript延遲解析的

主線程和非主線程的解析時(shí)間,以及運(yùn)行“跳過(guò)內(nèi)部函數(shù)”前后都得到了優(yōu)化。

隨時(shí)調(diào)用函數(shù)表達(dá)式

如前所述,打包器通常通過(guò)將模塊代碼封裝在一個(gè)它們即時(shí)調(diào)用的閉包中,來(lái)將多個(gè)模塊組合到一個(gè)文件中。這為模塊提供了隔離,允許它們像腳本中唯一的代碼一樣運(yùn)行。這些函數(shù)本質(zhì)上是嵌套的腳本;腳本執(zhí)行時(shí)這些函數(shù)會(huì)立即被調(diào)用。打包器通常以帶圓括號(hào)的函數(shù),即  (function(){…})(),的形式提供即時(shí)調(diào)用函數(shù)表達(dá)式(IIFEs,發(fā)音為“iffies”)。

由于這些函數(shù)在腳本執(zhí)行期間是立即需要的,所以預(yù)解析這些函數(shù)并不理想。在腳本的頂層執(zhí)行過(guò)程中,我們急需這些函數(shù)被編譯,所以我們會(huì)完全解析和編譯這些函數(shù)。這意味著,我們?cè)谇捌诮馕鲈娇?,代碼運(yùn)行時(shí)啟動(dòng)就越快,并且不會(huì)產(chǎn)生不必要的額外成本。

你可能會(huì)問(wèn),為什么不直接編譯調(diào)用的函數(shù)呢?雖然開(kāi)發(fā)人員在一個(gè)函數(shù)被調(diào)用時(shí)能很容易注意到它,但是對(duì)于解析器情況則不同。解析器在開(kāi)始解析函數(shù)之前需要決定該函數(shù)是需要立即編譯還是推遲編譯。語(yǔ)法中存在的歧義使得簡(jiǎn)單地快速掃描到函數(shù)末尾變得很困難,而且成本很快就與常規(guī)預(yù)解析的成本一樣。

因此V8有兩個(gè)簡(jiǎn)單的模式,它可以將函數(shù)識(shí)別為隨時(shí)調(diào)用函數(shù)表達(dá)式(PIFEs,發(fā)音為“piffies”),這樣它會(huì)快速解析并編譯一個(gè)函數(shù):

如果一個(gè)函數(shù)是一個(gè)帶圓括號(hào)的函數(shù)表達(dá)式,即(function(){…}),我們假設(shè)它將被調(diào)用。我們一看到這個(gè)模式的開(kāi)始,即(function,就立即做出這個(gè)假設(shè)。

在V8 v5.7 / Chrome  57中我們也檢測(cè)了由UglifyJS生成的模式!function(){…}(),function(){…}(),function(){…}()。一旦我們看到!function或者function后面如果緊跟著一個(gè)PIFE,那么這個(gè)檢測(cè)就起作用了。

由于V8會(huì)立即編譯PIFEs,所以它們可以被用作配置文件導(dǎo)向的反饋[2],通知瀏覽器啟動(dòng)需要哪些函數(shù)。

當(dāng)V8還在預(yù)解析內(nèi)部函數(shù)時(shí),一些開(kāi)發(fā)人員已經(jīng)注意到JS解析對(duì)啟動(dòng)的影響相當(dāng)大。optimize-js包會(huì)基于靜態(tài)啟發(fā)式將函數(shù)轉(zhuǎn)換為PIFEs。這個(gè)包的創(chuàng)建對(duì)V8的負(fù)載性能有很大的影響。通過(guò)在V8  v6.1上運(yùn)行optimize-js提供的基準(zhǔn)測(cè)試,我們復(fù)制了這些結(jié)果,你只需要查看縮小的腳本。

V8是怎么快速地解析JavaScript延遲解析的

急切地解析和編譯PIFEs會(huì)導(dǎo)致冷啟動(dòng)和熱啟動(dòng)稍微快一些 (***和第二頁(yè)加載,測(cè)量總的解析+編譯+執(zhí)行時(shí)間)。但是,由于對(duì)解析器的顯著改進(jìn),這在V8  v7.5上的好處要比在V8 v6.1上使用的好處小得多。

盡管如此,但我們現(xiàn)在不再需要重新解析內(nèi)部函數(shù),而且由于解析器變得更快,通過(guò)optimize-js獲得的性能改進(jìn)也大大降低。實(shí)際上,v7.5的默認(rèn)配置已經(jīng)比運(yùn)行在v6.1上的優(yōu)化版本快得多。即使在v7.5中,對(duì)于啟動(dòng)期間需要的代碼,少量使用PIFEs仍然很有用:  我們避免了預(yù)解析,因?yàn)槲覀兒茉缇椭罆?huì)需要這個(gè)函數(shù)。

盡管如此,但我們現(xiàn)在不再需要重新解析內(nèi)部函數(shù),而且由于解析器變得更快,通過(guò)optimize-js獲得的性能改進(jìn)也大大降低。實(shí)際上,v7.5的默認(rèn)配置已經(jīng)比運(yùn)行在v6.1上的優(yōu)化版本快得多。即使在v7.5中,對(duì)于啟動(dòng)期間需要的代碼,少量使用PIFEs仍然很有用: 我們避免了預(yù)解析,因?yàn)槲覀兒茉缇椭罆?huì)需要這個(gè)函數(shù)。

optimize-js基準(zhǔn)測(cè)試結(jié)果并不能準(zhǔn)確地反映實(shí)際情況。腳本是同步加載的,整個(gè)解析+編譯時(shí)間都被計(jì)入加載時(shí)間。在實(shí)際環(huán)境中,你可能會(huì)使用<script>標(biāo)記來(lái)加載腳本。這使得Chrome的預(yù)加載器能夠在腳本被計(jì)算之前就發(fā)現(xiàn)它,并在不阻塞主線程的情況下下載、解析和編譯該腳本。我們決定急切地編譯的所有東西都是在主線程之外自動(dòng)編譯的,這樣就會(huì)確保計(jì)入啟動(dòng)時(shí)間的值最小化。使用非主線程腳本編譯來(lái)運(yùn)行會(huì)放大使用PIFEs的影響。

但是,這樣做仍然有成本,特別是內(nèi)存成本,所以急切地編譯所有東西并不是一個(gè)好主意:

V8是怎么快速地解析JavaScript延遲解析的

急切地編譯所有JavaScript會(huì)付出巨大的內(nèi)存代價(jià)。

雖然在啟動(dòng)期間為需要的函數(shù)添加圓括號(hào)是一個(gè)好主意(例如,基于配置的啟動(dòng)),但是使用像optimize-js這樣的包來(lái)應(yīng)用簡(jiǎn)單的靜態(tài)啟發(fā)式并不是一個(gè)好主意。例如,它假設(shè)一個(gè)函數(shù)在啟動(dòng)期間被調(diào)用,如果它是一個(gè)函數(shù)調(diào)用的參數(shù)。但是,如果這樣一個(gè)函數(shù)實(shí)現(xiàn)了一個(gè)只需要很長(zhǎng)時(shí)間的完整模塊,那么最終會(huì)編譯太多。過(guò)于急切地編譯對(duì)性能沒(méi)有好處: 沒(méi)有延遲編譯的V8會(huì)顯著地降低加載時(shí)間。此外,當(dāng)UglifyJS和其它minifiers(最小化器)從不是IIFEs的PIFEs中刪除括號(hào)時(shí),也就刪除了本可以應(yīng)用于通用模塊定義樣式模塊的有用提示,這樣一來(lái),optimize-js的一些好處就帶來(lái)了問(wèn)題。這可能是minifiers應(yīng)該修復(fù)的一個(gè)問(wèn)題,以便在急切地編譯PIFEs的瀏覽器上獲得***的性能。

“V8是怎么快速地解析JavaScript延遲解析的”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識(shí)可以關(guān)注億速云網(wǎng)站,小編將為大家輸出更多高質(zhì)量的實(shí)用文章!

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI