精讀《V8 引擎 Lazy Parsing》

1. 引言

本周精讀的文章是 V8 引擎 Lazy Parsing,看看 V8 引擎為了優化性能,做了怎樣的嘗試吧!

這篇文章介紹的優化技術叫 preparser,是通過跳過不必要函數編譯的方式優化性能。

2. 概述 & 精讀

解析 Js 發生在網頁運行的關鍵路徑上,因此加速對 JS 的解析,就可以加速網頁運行效率。

然而并不是所有 Js 都需要在初始化時就被執行,因此也不需要在初始化時就解析所有的 Js!因為編譯 Js 會帶來三個成本問題:

  1. 編譯不必要的代碼會占用 CPU 資源。
  2. 在 GC 前會占用不必要的內存空間。
  3. 編譯后的代碼會緩存在磁盤,占用磁盤空間。

因此所有主流瀏覽器都實現了 Lazy Parsing(延遲解析),它會將不必要的函數進行預解析,也就是只解析出外部函數需要的內容,而全量解析在調用這個函數時才發生。

預解析的挑戰

本來預解析也不難,因為只要判斷一個函數是否會立即執行就可以了,只有立即執行的函數才需要被完全解析。

使得預解析變復雜的是變量分配問題。原文通過了堆棧調用的例子說明原因:

Js 代碼的執行在堆棧上完成,比如下面這個函數:

function f(a, b) {const c = a + b;return c;
}function g() {return f(1, 2);// The return instruction pointer of `f` now points here// (because when `f` `return`s, it returns here).
}

這段函數的調用堆棧如下:

TB1gNCsRVYqK1RjSZLeXXbXppXa-173-333.svg

首先是全局 This globalThis,然后執行到函數 f,再對 a b 進行賦值。在執行 f 函數時,通過 <rip g>(return instruction pointer) 保存 g 堆棧狀態,再保存堆棧跳出后返回位置的指針 <save fp>(frame pointer),最后對變量 c 賦值。

這看上去沒有問題,只要將值存在堆棧就搞定了。但是將變量定義到函數內部就不一樣了:

function make_f(d) {// ← declaration of `d`return function inner(a, b) {const c = a + b + d; // ← reference to `d`return c;};
}const f = make_f(10);function g() {return f(1, 2);
}

將變量 d 申明在函數 make_f 中,且在返回函數 inner 中用到了 d。那么函數的調用棧就變成了這樣:

TB1HiuGR4YaK1RjSZFnXXa80pXa-428-292.svg

需要創建一個 context 存儲函數 f 中變量 d 的值。

也就是說,如果一個在函數內部定義的變量被子 Scope 使用時,Js 引擎需要識別這種情況,并將這個變量值存儲在 context 中。

所以對于函數定義的每一個入參,我們需要知道其是否會被子函數引用。也就是說,在 preparser 階段,我們只要少能分析出哪些變量被內部函數引用了。

難以分辨的引用

預處理器中跟蹤變量的申明與引用很復雜,因為 Js 的語法導致了無法從部分表達式推斷含義,比如下面的函數:

function f(d) {function g() {const a = ({ d }

我們不清楚第三行的 d 到底是不是指代第一行的 d。它可能是:

function f(d) {function g() {const a = ({ d } = { d: 42 });return a;}return g;
}

也可能只是一個自定義函數參數,與上面的 d 無關:

function f(d) {function g() {const a = ({ d }) => d;return a;}return [d, g];
}

惰性 parse

在執行函數時,只會將最外層執行的函數完全編譯并生成 AST,而對內部模塊只進行 preparser

// This is the top-level scope.
function outer() {// preparsedfunction inner() {// preparsed}
}outer(); // Fully parses and compiles `outer`, but not `inner`.

為了允許惰性編譯函數,上下文指針指向了 ScopeInfo 的對象(從代碼中可以看到,ScopeInfo 包含上下文信息,比如當前上下文是否有函數名,是否在一個函數內等等),當編譯內部函數時,可以利用 ScopeInfo 繼續編譯子函數。

但是為了判斷惰性編譯函數自身是否需要一個上下文,我們需要再次解析內部的函數:比如我們需要知道某個子函數是否對外層函數定義的變量有所引用。

這樣就會產生遞歸遍歷:

TB1uCOPR7voK1RjSZFwXXciCFXa-960-540.svg

由于代碼總會包含一些嵌套,而編譯工具更會產生 IIFE(立即調用函數) 這種多層嵌套的表達式,使得遞歸性能比較差。

而下面有一種辦法可以將時間復雜度簡化為線性:將變量分配的位置序列化為一個密集的數組,當惰性解析函數時,變量會按照原先的順序重新創建,這樣就不需要因為子函數可能引用外層定義變量的原因,對所有子函數進行遞歸惰性解析了。

按照這種方式優化后的時間復雜度是線性的:

TB1VS5LR7voK1RjSZFNXXcxMVXa-960-540.svg

針對模塊化打包的優化

由于現代代碼幾乎都是模塊化編寫的,構建起在打包時會將模塊化代碼封裝在 IIFE(立即調用的閉包)中,以保證模擬模塊化環境運行。比如 (function(){....})()

這些代碼看似在函數中應該惰性編譯,但其實這些模塊化代碼從一開始就要被編譯,否則反而會影響性能,因此 V8 有兩種機制識別這些可能被立即調用的函數:

  1. 如果函數是帶括號的,比如 (function(){...}),就假設它會被立即調用。
  2. 從 V8 v5.7 / Chrome 57 開始,還會識別 uglifyJS 的 !function(){...}(), function(){...}(), function(){...}() 這種模式。

然而在瀏覽器引擎解析環境比較復雜,很難對函數進行完整字符串匹配,因此只能對函數頭進行簡單判斷。所以對于下面這種匿名函數的行為,瀏覽器是不識別的:

// pre-parser
function run(func) {func()
}run(function(){}) // 在這執行它,進行 full parser

上面的代碼看上去沒毛病,但由于瀏覽器只檢測被括號括住的函數,因此這個函數不被認為是立即執行函數,因此在后續執行時會被重復 full-parse。

也有一些代碼輔助轉換工具幫助 V8 正確識別,比如 optimize-js,會將代碼做如下轉換。

轉換前:

!function (){}()
function runIt(fun){ fun() }
runIt(function (){})

轉換后:

!(function (){})()
function runIt(fun){ fun() }
runIt((function (){}))

然而在 V8 v7.5+ 已經很大程度解決了這個問題,因此現在其實不需要使用 optimize-js 這種庫了~

4. 總結

JS 解析引擎在性能優化做了不少工作,但同時也要應對代碼編譯器產生的特殊 IIFE 閉包,防止對這種立即執行閉包進行重復 parser。

最后,不要試圖總是將函數用括號括起來,因為這樣會導致惰性編譯的特性無法啟用。

討論地址是:精讀《V8 引擎 Lazy Parsing》 · Issue #148 · dt-fe/weekly

如果你想參與討論,請 點擊這里,每周都有新的主題,周末或周一發布。前端精讀 - 幫你篩選靠譜的內容。

關注 前端精讀微信公眾號

TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg

special Sponsors

  • DevOps 全流程平臺

版權聲明:自由轉載-非商用-非衍生-保持署名(創意共享 3.0 許可證)

轉載于:https://www.cnblogs.com/ascoders/p/10752180.html

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/449166.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/449166.shtml
英文地址,請注明出處:http://en.pswp.cn/news/449166.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Git和SVN的區別,Git的使用方法大全

什么是Git: Git 是一個開源的分布式版本控制系統&#xff0c;用于敏捷高效地處理任何或小或大的項目。 Git 是 Linus Torvalds 為了幫助管理 Linux 內核開發而開發的一個開放源碼的版本控制軟件。 Git 與常用的版本控制工具 CVS, Subversion 等不同&#xff0c;它采用了分布…

詳解 springboot - 查看、修改內置 tomcat 版本

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 1、解析Spring Boot父級依賴 ?123456<parent> <groupId>org.springframework.boot</groupId> <artifactId>sp…

做生意的技巧 年入百萬不是夢(圖)

先介紹一下背景&#xff1a;這個表弟是土妖親大姨家的&#xff0c;從小不愛學習&#xff0c;但是腦子活絡。 現在在江蘇省泰州市姜堰區的一個農貿市場&#xff0c;開一個小餐館。餐館面積50多平米&#xff0c;年收入120萬左右。 少即是多——“我的小飯店只賣25種菜” 表弟…

reboot重啟失敗的解決方法

今天突然碰到用reboot命令不能重啟&#xff0c;上網找原因&#xff1a; reboot不能重啟可能是內核正在執行一些進程&#xff0c;reboot發送的信號被阻塞了&#xff0c;估計等一會內核從內核空間跳到用戶空間的時候&#xff0c;發現有信號被阻塞了&#xff0c;再執行這個阻塞的信…

BUAA-OO 第二單元作業“電梯調度”總結與思考

一、需求分析 利用java線程的相關知識實現 1&#xff09;單部多線程傻瓜調度&#xff08;FAFS&#xff09;電梯 2&#xff09;單部多線程可捎帶調度&#xff08;ALS&#xff09;電梯 3&#xff09;多部多線程智能&#xff08;SS&#xff09;調度電梯 二、思路分析 1、基于度量的…

解決報錯 javax.persistence.TransactionRequiredException: Executing an update/delete query

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 報錯如題。 場景是我想要執行一條很簡單的刪除語句。 JPA方式中使用本地sql , 寫法如下&#xff1a; ModifyingQuery("delete fr…

linux基礎知識點總結-最實用!(環境搭建,系統分區,常用命令,vim文本)

Linux系統介紹&#xff1a; Linux管理硬盤的能力非常強&#xff0c;所以我們看到的只有一個分區。 / 根目錄&#xff0c;所有文件都存儲在它下面 /bin 存儲著系統命令 /dev 設備文件&#xff08;一切皆文件&#xff09; /home 用戶主目錄&#xff0c;會自動生成用戶同名目錄 /…

遞歸實現進制轉換(C++版)

上次呢&#xff0c;我們留下了一道題&#xff0c;今天我們來一起看一看&#xff1a; 題目鏈接&#xff1a;https://www.cnblogs.com/gaozirong/p/10547434.html 這是我寫的程序&#xff0c;大家可以對照參考一下&#xff08;C&#xff09;&#xff1a; #include<bits/stdc.h…

解決 mysql 插入數據報錯: Cannot add or update a child row: a foreign key constraint fails

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 場景&#xff1a;我的情況是主表為用戶 user 表&#xff0c;從表為職位 job 表&#xff0c;其中 job 表有一個外鍵為 user 表的主鍵。 …

猶太人從未透露的12個秘密(圖)

中國人喜歡攢錢&#xff0c;西方人熱衷花錢&#xff0c;只有猶太人精于賺錢。 他們認為&#xff1a;唯有運用智慧賺錢&#xff0c;才是真正的致富之道。他們的經商智慧風靡全球&#xff0c;造就了無數的商業巨子。本文通過解讀猶太人精妙絕倫的經商之道&#xff0c;從人性、道…

vim文本編輯器的配置vimrc

在行底模式下對vim的設置只是臨時有效&#xff0c;如果想長期有效需要把這些設置語句寫入配置文件(~/.vimrc)中。 打開vim的配置&#xff1a;vim ~/.vimrc 進行編輯&#xff1a; " 顯示行號 set number" tab鍵寬度 set tabstop4" 設置自動縮進 set autoindent…

IDEA 錯誤:找不到或無法加載主類

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 從昨天開始使用IDEA開始就一直在搭建java環境&#xff0c;許久沒有使用過java&#xff0c;剛開始有些生疏&#xff0c;先建了一個最簡單…

Android 第一篇

在Android的世界里以一只萌級小菜鳥的身份起飛&#xff0c;在后面的路途中不斷的成長。轉載于:https://www.cnblogs.com/ming-michelle/p/10558328.html

如何吸引財富呢?請做這六件事吧!

一&#xff1a;投資你的債務 有一則故事到處流傳&#xff1a;當聲名浪藉的威利被問到為什么要搶劫銀行時&#xff0c;他回答道&#xff1a;“因為這里有錢。”威利可能是個惡棍&#xff0c;但不是個笨蛋。他選對了目標。不過如能夠到銀行里投資&#xff0c;而不是到這里搶劫&am…

vscode解決中文亂碼

打開文件時出現亂碼 文件->首選項->設置&#xff0c;然后在右邊用戶設置里打開settings.json &#xff0c;輸入&#xff1a; “files.autoGuessEncoding”: true, CtrlS保存一下&#xff0c;就搞定了&#xff01; 沒有做很大的修改&#xff0c;可以正常使用就行 {&qu…

Centos 7源碼編譯搭建Nginx

一、Nginx入門介紹 1. Nginx&#xff08;engine x&#xff09;&#xff1a;[?end??nks] 2. Nginx 是 Igor Sysoev 為俄羅斯訪問量第二的 Rambler.ru 站點開發的&#xff0c;第一個公開版發布于2014年10月4日 3. 主要功能 1&#xff09;HTTP 服務器&#xff08;包含動靜分離…

數據備份、pymysql模塊

----------mysql數據備份------------- #1. 物理備份&#xff1a; 直接復制數據庫文件&#xff0c;適用于大型數據庫環境。但不能恢復到異構系統中如Windows。 #2. 邏輯備份&#xff1a; 備份的是建表、建庫、插入等操作所執行SQL語句&#xff0c;適用于中小型數據庫&#xff0…

優雅的找出ArrayList中重復的元素

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 方法1 代碼&#xff1a; public class Main { public static void main(String[] args){ List<String> list new ArrayList&…

全球通吃的九大黃金專業

出國留學的同學在選專業時尤其搖擺不定&#xff0c;選擇時需要根據以后的就業發展來挑選適合自己的專業。專業選擇得當與否&#xff0c;決定著留學生回國發展是否順利&#xff0c;決定著巨額投資是否物有所值&#xff0c;以下九類比較有發展潛力的黃金留學專業&#xff0c;供大…

C語言里最基礎的關鍵字

內建類型&#xff1a; void、char、short、int、long、float、double 自建類型&#xff1a; struct、union、enum、sizeof 類型限定符&#xff1a; auto、const、static、volatile、register、extern、typedef、signed、unsigned 分支&#xff1a; if、else、switch、case、def…