JavaScript 为什么快--第二篇

  1. 云栖社区>
  2. 博客列表>
  3. 正文

JavaScript 为什么快--第二篇

秦粤 2018-08-10 14:24:08 浏览1620 评论0

摘要: 上一篇,我们介绍了 V8 引擎的执行管道架构。本篇将着重介绍 V8 的语法解析过程。原视频上一篇是产品经理思维;本篇则是理工科思维;语法解析阶段对于前端来说尤其重要,相对 Noder 来说较弱,因为 parser 只会影响应用启动和前期的运行阶段。

上一篇,我们介绍了 V8 引擎的执行管道架构。本篇将着重介绍 V8 的语法解析过程。原视频
上一篇是产品经理思维;本篇则是理工科思维;
语法解析阶段对于前端来说尤其重要,相对 Noder 来说较弱,因为 parser 只会影响应用启动和前期的运行阶段。
对于前端同学来说,经常习惯性的引入一些很大的库,而只使用了其中1,2个函数。例如 lodash。这样对性能的影响到底有多大?

还是结论先行

  1. V8的语法解析有2种模式:eager 解析器(全面)和 lazy 预解析器(快速)。虽然 lazy 解析比 eager 快一倍,但是lazy可能导致需要1.5倍的解析时间;(lazy 预解析后,还需要 eager 解析一次)。你可以用Optimize.js强制 eager 运行
  2. JavaScript 的语法解析速度为:1MB/S。解析400k JavaScript,需要大概370ms。可以通过 chrome 浏览器地址栏 chrome://tracing 查看具体时间;
  3. 前端页面运行的 JavaScript 代码尽量少;解析器也有缓存,缓存字节码,如果采用 bundle 的话,更新 bundle 会导致整个 bundle 失效。

计算机编译原理简单介绍

由于本篇需要部分计算机编译原理背景知识。所以感觉需要补充一下,计算机编译原理,将人能读懂的代码转换成机器能读懂的代码,机器执行时只认识机器语言指令。
通常计算机高级语言都需要经过:源程序->语法解析->中间代码生成->代码优化->目标代码生成->目标程序。对应V8也不例外:JS源代码->语法解析->生成字节码->编译器->转化器->运行代码。
语法解析阶段生成语法树和作用域,就是将我们的每行代码变成语法树状结构,来消除歧义,代码分析,绑定作用域。
可以通过esprima看看JavaScript的语法树什么样子:http://esprima.org/demo/parse.html#

本篇主要介绍V8的语法解析过程,产物就是字节码(中间代码)。下一篇介绍V8的编译器运行。

JavaScript 语法解析 - lazy 要比 eager 好吗?

什么是 JavaScript 语法解析?

我们从上一篇的 JavaScript 执行管道,下图红色的部分就是语法解析的过程。实际就是 JavaScript 的编译阶段。虽然编译过程不参与“ JavaScript 的运行阶段(下图蓝色部分)”,但作为动态脚本语言,JavaScript 的解析在代码变更和实际运行时,还是会触发语法解析的。

15335656973482

我们为何要关心解析?

  • 一个典型的单页 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%
  • 实际上我们只需要

    1. 解析编译正确的函数;
    2. 最小化我们失败的代价;
  • 在此基础上迭代

Web 开发者如何利用 V8 的解析器?

使用更少的代码!

  • JavaScript 的启动性能;
  • 使用更少的 JavaSCript: 使用 Chrome Dev Tools 的 code coverage 功能;
  • 衡量你的代码解析开销:chrome://tracingv8.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

用云栖社区APP,舒服~

【云栖快讯】诚邀你用自己的技术能力来用心回答每一个问题,通过回答传承技术知识、经验、心得,问答专家期待你加入!  详情请点击

网友评论

秦粤
文章2篇 | 关注4
关注
云数据库PPAS版,是阿里云与EnterpriseDB公司合作基于PostgreSQL高度兼... 查看详情
PostgreSQL被业界誉为“最先进的开源数据库”,面向企业复杂SQL处理的OLTP在线事... 查看详情
提供一种性能卓越、稳定、安全、便捷的计算服务,帮助您快速构建处理能力出色的应用,解放计算给服... 查看详情
为您提供简单高效、处理能力可弹性伸缩的计算服务,帮助您快速构建更稳定、安全的应用,提升运维效... 查看详情
阿里云总监课正式启航

阿里云总监课正式启航