深入浅出 JS 编译

338 阅读8分钟

前言

一个老生常谈的问题,JS究竟是不是解释型语言,这个问题的本质其实还是得看执行 JS 的代码引擎究竟是如何处理 JS 代码的。如 V8引擎,SpiderMonkey,JSC 等,都有 JIT(just-in-time) Compiler,即将热点代码从字节码(Bytecode)进一步处理成机器码(Machine Code),提升代码执行速度,从这个角度看,JS 就不是常规意义上的解释型语言了,而是有了编译型语言的特点。

但是 JS 又与传统的编译性语言不同,它不是提前编译的,编译结果也不能在分布式系统中移植 —— 《你不知道的JavaScript》

严格来说,JS是即时编译性语言,因为编译后没有生成编译文件,而是马上执行了,和传统编译型语言有区别,说是解释性语言

JS 就是这么一个充满可能性的语言,接下来,我们就深入浅出一下 JS 编译原理。

什么是编译

Pasted image 20250715222959.png One language to another 将一种语言变成其他语言

  • A: Source 2 Source
    • 如 TS -> JS
    • JSX -> JS
    • ES6、ES7 语法降级 ES5
  • B: Source 2 Bytecode
    • 如 解释性语言
  • C: Source 2 MachineCode
    • 如 编译性语言
  • D: Bytecode 2 MachineCode
    • JIT

其中 Source 2 Source 也称之为预编译,通常指的是在开发阶段或构建阶段(Build Time),使用工具将一种形式的源代码转换为另一种形式的源代码的过程。这个过程也常被称为 “转译” (Transpiling) ,这个词其实更准确。

接下来的文章中,不明确指出的话,编译不包括预编译,即 A 的环节,因为这一步的执行者通常不是 JS 引擎,而是某些开发/构建工具 (例如 Babel, TypeScript Compiler (tsc), Webpack, Vite 等)。

JS代码编译大致流程

Pasted image 20250716153525.png

  1. 源代码 → 解析器 → 抽象语法树 (AST)

  2. AST → 解释器 (如 Ignition) → 字节码

  3. 解释执行字节码,同时监控器分析代码运行情况

    3.1 在此过程中,引擎会持续收集类型反馈(Type Feedback),记录函数被调用的次数、传入参数的类型以及对象的形状(Hidden Classes)

  4. 识别热点代码 → JIT编译器 (如 TurboFan) → 优化后的机器码

  5. 如果优化假设失败 → 去优化 → 回退到字节码执行

Source Code -> AST

当JavaScript引擎获取到JS Source Code后,编译的第一步便是被解析器 parser 解析(Parsing)。这个阶段主要包含两个核心环节:

  • 词法分析(Lexical Analysis):此阶段会将您的源代码字符串分解成一个个有意义的单元,称为词法单元(Tokens),这个过程也被称为分词(Tokenization)
  • 语法分析(Syntax Analysis):在获得词法单元流后,语法分析器会根据JavaScript的语法规则,将这些词法单元组合成一个树形结构,即抽象语法树(Abstract Syntax Tree, AST)。AST是源代码语法结构的抽象表示,它精确地反映了代码的层次和关系。例如,一个变量声明语句在AST中会是一个节点,它包含了变量名和被赋予的值等信息。开发者可以利用像 AST Explorer 这样的工具来直观地查看代码生成的AST。

AST -> Bytecode

直接执行抽象语法树AST也是可以的,但是其由于其结构特点,导致执行效率低,所以通常要处理成 Bytecode字节码

  • AST 树是重型的数据结构,本身内存占用就比较高
  • 树结构,节点和节点之间并不是相邻的,内存不是相邻的,导致CPU 的 Cache 缓存命中率低
  • 树遍历的过程中有很多的节点跳转,需要很多的指令访问

AST 被字节码生成器(generator)、字节码优化器(opimizer)处理后,生成可以被字节码解释器(interpreter)解释执行的字节码。正所谓磨刀不误砍柴工,处理一下后 Bytecode 的执行效率有了较大提升。

采用字节码的好处在于:

  • 减少内存占用:相比于AST或者直接生成机器码,字节码更加紧凑,可以显著降低内存消耗。
  • 提升启动速度:生成字节码的速度远快于生成优化后的机器码,这使得代码可以更快地开始执行,优化了应用的启动性能。

字节码是一种与特定物理机器无关的中间代码,它比AST更接近机器码,但又比机器码更具平台无关性。可以将其理解为JavaScript虚拟机(VM)能够直接理解和执行的指令集。

字节码本质就是内存中的一条条数据,每条数据代表一条指令,去模拟 CPU执行指令

JS引擎在初次首先解释执行字节码(解释型执行)。在这个阶段,代码会逐行执行,但这个执行过程相对机器码直接执行而言仍然较慢。

ByteCode -> Machine Code

JS 引擎将 AST 处理成字节码后,还会监控相关代码的执行状态,识别出频繁执行的代码块(即所谓的 "热代码"),则会进一步将热点代码编译成 Machine Code机器码。这个过程称为 即时编译(JIT),编译后的机器码可以被直接执行,而不再需要解释。

某些代码段的执行模式已经固定,JS 引擎还会对其进行更多优化。例如,使用 内联缓存(Inline Caching) 等技术,提高访问属性或方法的速度。

Machine Code-> ByteCode (去优化 Deoptimization)

JIT编译器的优化是建立在一些假设(Assumptions) 之上的。例如,一个函数在多次调用中都接收了相同类型的参数,编译器可能会假设这个函数未来也只会接收这种类型的参数,并据此生成优化的机器码。

但是由于 JS 是一门动态的语言,有可能在执行过程中可能会有不同数据类型或者 Shape

可以简单理解为对引用类型的一种抽象,对于键名一样的引用类型有同样的 Shape 对 Shape 感兴趣的,可以 进一步看参考资料中的两个 YouTube 的视频

那么原先编译成的机器码就失效了,那么就要回退成更灵活的 Ignition 字节码或较低级的编译器执行,也就是deoptimize 去优化的过程。

频繁的去优化(去优化循环)会导致性能断崖式下跌,甚至比纯解释执行更慢

核心优化技术

  • 隐藏类 (Hidden Classes / Maps):JavaScript 对象是动态的,但 V8 会假设对象遵循可预测的模式。每当对象添加属性时,它会通过一个转换链迁移到新的隐藏类。这使得编译器能以固定内存偏移量直接访问属性,而无需昂贵的哈希表查找。

  • 内联缓存 (Inline Caching, IC):IC 会在代码的调用点“记住”上一次看到的隐藏类和属性偏移量。如果下次遇到相同的隐藏类(单态状态),则可以直接使用缓存结果,速度极快。

  • 投机优化 (Speculative Optimization):TurboFan 会赌代码的行为(如:这个变量永远是整数)并生成高度优化的机器码

前端常见 JS 引擎介绍

主流的JS引擎都有 ByteCode2MachineCode 和MachineCode2ByteCode 的环节,都有某种解析器(parser)、解释器(interpreter、) 编译器管道(compiler pipeline) 但具体处理细节是有差异的,可以简单了解一下。

Pasted image 20250716153708.png

V8

Pasted image 20250716153728.png V8 的解释器叫 Ignition,JIT 编译器叫 TurboFan

这图有点年代了, 懒得自己画了 V8 也如其他家引擎一样在持续迭代中, 新增了很多东西:

  • Tier 0: Ignition (解释器):执行字节码并收集数据
  • Tier 1: Sparkplug (基线编译器):当代码变“暖”时,Sparkplug 会将字节码直接编译为未经优化的机器码. 它不进行复杂的分析,旨在平滑解释执行与高级优化之间的性能差距
  • Tier 2: Maglev (中级编译器):对于变“热”的代码,Maglev 利用 Ignition 收集的类型反馈进行静态单赋值(SSA)形式的优化。它的目标是“足够快地生成足够好的代码”
  • Tier 3: TurboFan (顶级优化编译器):对于极热的代码路径,TurboFan 会进行激进的投机性优化。它使用“节点海”(Sea of Nodes)架构,应用内联(Inlining)、死代码删除和循环展开等高级编译技术

SpiderMonkey

Pasted image 20250716153842.png SpiderMonkey 的做法有所不同,有两个优化编译器

JavaScriptCore

Pasted image 20250716154206.png JSC 使用三个优化编译器,它从 LLint (low-level interpreter)生成字节码的低级解释器开始

参考资源

本篇的图片和思路来源于以下,侵权删

阿里工程师带你从零开始吃透编译技术:编译技术概述_哔哩哔哩_bilibili

JavaScript Engines: The Good Parts™ - Mathias Bynens & Benedikt Meurer - JSConf EU 2018

Understanding the V8 JavaScript Engine