精读《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.mzph.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…