在上一节我们提到,V8 引擎接收 JavaScript 源码后,经过解析生成 AST,再转换为字节码。但字节码仍然需要解释器一条条执行,性能远不如原生机器码。为了让 JavaScript 跑得更快,现代引擎引入了一种混合策略——即时编译(Just-In-Time Compilation,JIT)。
为什么需要混合优化?
先来看两种极端的执行方式:
- 纯解释执行:启动极快,代码一写完就能跑,但每次执行都需要反复解析、翻译,对于循环等频繁执行的代码,浪费大量 CPU 时间。
- 纯提前编译(AOT):在运行前将所有代码编译为机器码,启动慢,但运行速度快。不过 JavaScript 是动态类型语言,很多类型信息只有在运行时才能确定,提前编译无法做出最优假设。
JIT 的设计目标很务实:兼顾启动速度和运行性能。
JIT 的核心流程
- 快速启动:引擎先用解释器(如 V8 的 Ignition)将字节码快速翻译为机器指令并执行。这一阶段无需等待任何编译,用户能立刻获得响应。
- 热点探测:在执行过程中,引擎会持续监控哪些代码段被执行得最频繁——这些被称为“热点”(hot code)。V8 通过内置的计数器统计函数调用次数和循环迭代次数,当超过预设阈值时,就认为该段代码是“热”的。
- 即时编译:遇到热点代码,引擎启动优化编译器(如 V8 的 TurboFan),以该段代码为输入,结合运行时收集的类型信息,生成高度优化的机器码。这个过程发生在运行时,因此被称为“即时”。
- 替换执行:优化后的机器码会替换掉原本的字节码解释入口,后续对该函数的调用将直接执行高效的机器码,性能大幅提升。
- 去优化(Deoptimization):JavaScript 的动态类型意味着之前假设的类型信息可能在未来失效。例如,一个函数最初总是接收整数参数,JIT 据此做了针对性优化;但后来传入了一个字符串,优化假设被打破。此时引擎会丢弃之前的优化代码,回退到解释执行或重新编译,这个过程叫做“去优化”。虽然有一定开销,但确保了语言的灵活性不受损失。
对开发者的实际意义
理解 JIT 机制,有助于写出对引擎更友好的代码:
- 保持类型稳定:尽量让一个函数或对象的属性结构保持不变。例如,构造函数中始终按相同顺序初始化同样的属性,避免动态添加/删除属性导致“隐藏类”频繁变化,从而减少去优化。
- 热路径优化意识:对于性能敏感的代码(如核心渲染循环、高频事件处理器),尽量让它成为 JIT 眼中的“热点”,使得它被充分优化。
- 一次思考,持续受益:JIT 的优化在运行时自动发生,开发者不需要手动标记哪些代码需要编译。但养成良好的编码习惯,能减少引擎“猜测错误”的概率,避免运行时性能抖动。
JIT 是 JavaScript 能够同时追求“快速启动”和“接近原生速度”的关键技术。它打破了传统“解释型语言一定慢”的刻板印象,让 Web 应用也能拥有流畅的交互和高性能的复杂逻辑。在后续章节中讨论性能优化时,很多底层原理其实都源于 JIT 的工作方式。