上一篇, 我们介绍了 V8 引擎的执行管道架构. 本篇将着重介绍 V8 的语法解析过程. 原视频 https://www.youtube.com/watch?v=Fg7niTmNNLg
上一篇是产品经理思维; 本篇则是理工科思维;
语法解析阶段对于前端来说尤其重要, 相对 Noder 来说较弱, 因为 parser 只会影响应用启动和前期的运行阶段.
对于前端同学来说, 经常习惯性的引入一些很大的库, 而只使用了其中 1,2 个函数. 例如 lodash. 这样对性能的影响到底有多大?
还是结论先行
V8 的语法解析有 2 种模式: eager 解析器 (全面) 和 lazy 预解析器(快速). 虽然 lazy 解析比 eager 快一倍, 但是 lazy 可能导致需要 1.5 倍的解析时间;(lazy 预解析后, 还需要 eager 解析一次). 你可以用 Optimize.js https://github.com/nolanlawson/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 https://github.com/nolanlawson/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
来源: https://yq.aliyun.com/articles/624172