上一篇,我们介绍了 V8 引擎的执行管道架构。本篇将着重介绍 V8 的语法解析过程。原视频
上一篇是产品经理思维;本篇则是理工科思维;
语法解析阶段对于前端来说尤其重要,相对 Noder 来说较弱,因为 parser 只会影响应用启动和前期的运行阶段。
对于前端同学来说,经常习惯性的引入一些很大的库,而只使用了其中1,2个函数。例如 lodash。这样对性能的影响到底有多大?
还是结论先行
- V8的语法解析有2种模式:eager 解析器(全面)和 lazy 预解析器(快速)。虽然 lazy 解析比 eager 快一倍,但是lazy可能导致需要1.5倍的解析时间;(lazy 预解析后,还需要 eager 解析一次)。你可以用Optimize.js强制 eager 运行
- JavaScript 的语法解析速度为:1MB/S。解析400k JavaScript,需要大概370ms。可以通过 chrome 浏览器地址栏
chrome://tracing
查看具体时间; - 前端页面运行的 JavaScript 代码尽量少;解析器也有缓存,缓存字节码,如果采用 bundle 的话,更新 bundle 会导致整个 bundle 失效。
计算机编译原理简单介绍
由于本篇需要部分计算机编译原理背景知识。所以感觉需要补充一下,计算机编译原理,将人能读懂的代码转换成机器能读懂的代码,机器执行时只认识机器语言指令。
通常计算机高级语言都需要经过:源程序->语法解析->中间代码生成->代码优化->目标代码生成->目标程序。对应V8也不例外:JS源代码->语法解析->生成字节码->编译器->转化器->运行代码。
语法解析阶段生成语法树和作用域,就是将我们的每行代码变成语法树状结构,来消除歧义,代码分析,绑定作用域。
可以通过esprima看看JavaScript的语法树什么样子:http://esprima.org/demo/parse.html#。
本篇主要介绍V8的语法解析过程,产物就是字节码(中间代码)。下一篇介绍V8的编译器运行。
JavaScript 语法解析 - lazy 要比 eager 好吗?
什么是 JavaScript 语法解析?
我们从上一篇的 JavaScript 执行管道,下图红色的部分就是语法解析的过程。实际就是 JavaScript 的编译阶段。虽然编译过程不参与“ JavaScript 的运行阶段(下图蓝色部分)”,但作为动态脚本语言,JavaScript 的解析在代码变更和实际运行时,还是会触发语法解析的。
我们为何要关心解析?
-
一个典型的单页 Web 应用:
- 需要加载0.4MB的 JavaScript;
- 大约耗时370毫秒;(在手机型号 Moto G4 测试)
- ->语法解析的速度 ~ 1MB/s
V8 是如何处理 JavaScript 语法解析的? eager parse & lazy parse
这是 V8 的自己实现,为了提升 JavaScript 文件的语法解析速度;目前非 JavaScript 引擎的官方规范。
- 2种解析模式: eager (全面解析模式) 和 lazy (快速解析模式)
- 为什么解析 JavaScript 代码那么难?
2种解析器
-
解析器: 全面解析模式, "eager"
- 用于解析我们想编译的函数;
- 构建语法树;
- 构建函数作用域(Scopes);
- 找出所有语法错误;
-
预-解析器: 快速解析模式, "lazy"
- 用于跳过我们不想编译的函数们;
- 不构建语法树,会构建函数作用域,但不设置函数作用域中的变量引用(variable references)和变量申明(variable declarations);
- 解析速度,大约比eager解析器快2倍
- 找出限定的几种错误(没有遵守JavaScript的规范)
Lazy or eager?
lazy 预编译由前2位首字母决定;所以如果我们想跳过 lazy 触发 eager 编译,我们应该在前面加位操作符,例如'!|~'。我们直接看代码:
let a = 0; //顶层的代码都是 eager
// 立即执行函数表达式 IIFE = Immediately Invoked Function Expression
(function eager() {...})(); // 函数体是 lazy
// 顶层的函数非IIFE
function lazy() {...} // 函数体是 lazy
// 后续执行时
...
lazy(); // ->eager 开始解析和编译!
// 启示,通过这种方式触发eager解析
!function eager2() {...}, function eager3() {...} // All eager
// 错误的case!
let f2 = function lazy() {...}(); // 先触发了lazy 解析, 然后又eager解析
Lazy 和 Eager 为什么都很重要?
- 我们需要lazy解析器, 因为web页面会使用很多无关代码;(事实,摆手)
-
如何选择呢?
- 如果我们eager解析了我们无关代码,我们在浪费时间;
- 如果我们lazy解析了我们有关代码,我们将多支付预解析的时间:0.5 x 解析时间 + 1 x 解析时间 = 1.5 解析时间
- 假设我们只知道我们的启动代码,并不知道具体会执行哪些代码。(事实again,摆手)
强迫执行 eager 解析
- Optimize.js 用括号括住它认为将被执行的函数。
浏览器 | 使用 optimize-js 后通常启动速度提升 |
---|---|
Chrome 55 | 20.63% |
Edge 14 | 13.52% |
Firefox 50 | 8.26% |
Safari 10 | -1.04% |
-
实际上我们只需要
- 解析编译正确的函数;
- 最小化我们失败的代价;
- 在此基础上迭代
Web 开发者如何利用 V8 的解析器?
使用更少的代码!
- JavaScript 的启动性能;
- 使用更少的 JavaSCript: 使用 Chrome Dev Tools 的 code coverage 功能;
- 衡量你的代码解析开销:
chrome://tracing
和v8.runtime_stats
代码缓存 + Bundling
- 代码缓存: V8 会缓存经常使用的 JavaScript 的字节码;
- Bundling: 如果你更新了 bundle 的一部分代码,将失去整个 bundle 缓存;
- 避免使用
eval
Web 开发者: 使用 streaming
- 流式 JavaScript: 并行下载和解析;
-
体积大的 JavaScripts
- 尽可能早的异步读取;
- 确保流式 JavaScript 运转
chrome://tracing
括号黑魔法
-
使用括号技巧选中需要 eager 解析并编译的关键路径:
- 旧版本的 Chrome;
- 跨浏览器;
- 现在就要提升性能,立刻马上!(等不及我们去修复)
额外内容
- V8 解析器是一款 V8 递归下降编译器;
- 大约~15k 行C++ 还有 ~7k 行C, for the AST+Scopes