深度解析 AI 资讯 社区
源码解析

Hermes 源码解析

深入 Meta 开源的 Hermes JavaScript 引擎 —— 从词法分析到字节码生成,从 SSA 中间表示到并发垃圾回收,逐层解析一个为移动端优化的 JS 运行时的设计哲学与工程实现。

📅 2026-04-28 ⏱ 约 45 分钟 📦 基于 GitHub 源码分析

一、什么是 Hermes

Hermes 是 Meta(前 Facebook)开源的 JavaScript 引擎,专门为 React Native 应用的快速启动而设计。与 V8 和 JavaScriptCore 不同,Hermes 不追求峰值执行性能,而是将优化重心放在 启动时间内存占用 两个对移动端至关重要的指标上。

Hermes 的核心设计理念是提前编译(Ahead-of-Time Compilation):在应用构建阶段就将 JavaScript 编译为紧凑的字节码,运行时直接加载执行,跳过传统引擎在启动时的解析和编译开销。

为什么需要一个新的 JS 引擎?

React Native 应用在冷启动时需要加载和编译大量 JavaScript 代码。V8 的 JIT 编译器为长时运行的 Web 应用优化,但在移动端冷启动场景中反而成为瓶颈 —— 启动时花费大量时间在 JIT 编译上。Hermes 通过将编译前移到构建阶段,将这个成本从运行时完全消除。

从源码规模来看,Hermes 是一个中等规模的系统级项目:

模块语言核心目录
编译器(Parser → IR → Bytecode)C++lib/Parser, lib/IR, lib/BCGen
优化器C++lib/Optimizer
虚拟机(Runtime + GC)C++lib/VM
标准库实现C++ / JSlib/VM/JSLib, lib/InternalJavaScript
Android 集成Java / C++android/
调试支持C++ / Pythonlldb/

二、整体架构

2.1 编译流水线

Hermes 的编译流水线是一条清晰的线性管道,每个阶段的输入输出都有明确定义:

JavaScript 源码 │ ▼ ┌──────────────────┐ │ Lexer (词法分析) │ lib/Parser/JSLexer.cpp │ Source → Tokens │ └────────┬─────────┘ │ ▼ ┌──────────────────┐ │ Parser (语法分析) │ lib/Parser/JSParserImpl.h │ Tokens → AST │ (递归下降 LL(1)) └────────┬─────────┘ │ ▼ ┌──────────────────┐ │ Sema (语义分析) │ lib/Sema/ │ 名称解析 + 作用域 │ └────────┬─────────┘ │ ▼ ┌──────────────────┐ │ IRGen (IR 生成) │ lib/IRGen/ESTreeIRGen-*.cpp │ AST → SSA IR │ (9 个专门化的实现文件) └────────┬─────────┘ │ ▼ ┌──────────────────┐ │ Optimizer (优化) │ lib/Optimizer/Scalar/ │ 27 个优化 Pass │ (固定点迭代直到收敛) └────────┬─────────┘ │ ▼ ┌──────────────────┐ │ BCGen (字节码生成) │ lib/BCGen/HBC/ │ IR → HBC 字节码 │ (寄存器分配 + 指令选择) └────────┬─────────┘ │ ▼ ┌──────────────────┐ │ Runtime (运行时) │ lib/VM/ │ 字节码解释执行 │ (NaN-Boxing + Hades GC) └──────────────────┘

2.2 与 V8/JSC 对比

理解 Hermes 的设计取舍,最好的方式是与 V8 和 JavaScriptCore(JSC)对比:

维度V8JavaScriptCoreHermes
编译策略多层 JIT(Ignition → TurboFan)多层 JIT(LLInt → DFG → FTL)AOT 编译 + 解释执行
优化时机运行时热点检测运行时热点检测构建时静态优化
启动性能中等(需要 JIT 预热)中等极快(直接加载字节码)
峰值性能最高(TurboFan 机器码)高(FTL 机器码)中等(解释执行)
内存占用较高(JIT 代码缓存)中等最低(紧凑字节码)
目标场景浏览器 / Node.jsSafari / iOSReact Native 移动端
GCOrinoco(并发标记 + 增量压缩)Riptide(并发)Hades(并发标记 + 自由链表)

Hermes 的选择是一个清晰的工程权衡:放弃峰值性能换取启动速度和内存效率。对于 React Native 应用而言,这通常是正确的取舍 —— UI 框架的渲染瓶颈很少在 JS 执行速度上。

三、解析器

3.1 词法分析

Hermes 的词法分析器(Lexer)实现在 lib/Parser/JSLexer.cpp 中。它将 JavaScript 源码转换为 Token 流。

Lexer 面临的一个经典难题是 JavaScript 中 / 的歧义 —— 它既可以是除法运算符,也可以是正则表达式的开始。Hermes 通过 Parser 上下文来消歧:

// JavaScript 中的 / 歧义
let x = 10 / 2;       // 除法
let re = /pattern/g;   // 正则表达式

// Lexer 需要 Parser 的上下文才能正确判断
// 当期望的是表达式(如赋值号右侧),/ 开始正则
// 当期望的是运算符(如数字后面),/ 是除法

Lexer 还支持 Magic URL(特殊注释中的源映射 URL)和 Token 存储(用于工具支持)。

3.2 语法分析

Hermes 的 Parser 采用经典的递归下降 LL(1) 设计,实现在 lib/Parser/JSParserImpl.h 中。核心类是 JSParserImpl,它将 Token 流转换为 ESTree 格式的 AST。

Parser 的一个精巧设计是 Param 参数封装。JavaScript 的语法在不同上下文中有不同的行为(例如 in 在 for-in 循环中是关键字,在其他地方是运算符),Parser 使用位标志来高效传递这些上下文:

// Param 类使用位运算组合多个布尔参数
class Param {
  unsigned flags_;
  // 位标志示例:
  // AllowIn      = 1 << 0   // 是否允许 'in' 运算符
  // AllowYield   = 1 << 1   // 是否在生成器上下文中
  // AllowAwait   = 1 << 2   // 是否在 async 上下文中
  // AllowReturn  = 1 << 3   // 是否允许 return 语句
};

// 通过位运算组合和传递参数
Param paramIn = Param::AllowIn | Param::AllowYield;
parseExpression(paramIn);

Parser 支持多种 JavaScript 方言的扩展:

产出的 AST 遵循 ESTree 规范,这是 JavaScript AST 的事实标准。

四、中间表示 (IR)

4.1 类型系统

Hermes 的 IR 采用 SSA(Static Single Assignment) 形式,每个变量只被赋值一次。这是现代编译器优化的基础。

IR 的类型系统定义在 include/hermes/IR/IR.h 中,包含 14 种基本类型:

// Hermes IR 类型系统(简化表示)
enum class Type : uint16_t {
  Empty,        // TDZ(Temporal Dead Zone)标记
  Uninit,       // 未初始化
  Undefined,    // JavaScript undefined
  Null,         // JavaScript null
  Boolean,      // true / false
  String,       // 字符串
  Number,       // IEEE 754 双精度浮点
  BigInt,       // 任意精度整数
  Symbol,       // ES6 Symbol
  Environment,  // 闭包环境
  PrivateName,  // 类私有字段名
  FunctionCode, // 函数代码对象
  Object,       // JavaScript 对象
};

// 类型联合通过位掩码实现
// 例如 "string | number" 表示为:
Type stringOrNumber = Type::String | Type::Number;

位掩码类型联合的设计使得类型推断可以高效地进行集合运算(并集、交集、子集判断),这对优化器至关重要。

4.2 指令集

IR 指令定义在 include/hermes/IR/Instrs.def 中,涵盖了 JavaScript 语义的完整表达。指令分为几大类:

分类代表指令语义
控制流Branch, CondBranch, Switch, Return基本块间跳转和函数返回
栈操作AllocStack, LoadStack, StoreStack局部变量的栈分配和访问
帧操作LoadFrame, StoreFrame闭包捕获变量的访问
对象操作AllocObject, LoadProperty, StorePropertyJavaScript 对象创建和属性访问
函数调用CallInst, CreateFunction, CallBuiltinInst函数创建、调用和内建函数优化
异常处理TryStartInst, TryEndInst, CatchInst, ThrowInsttry/catch/finally 语义
生成器CreateGenerator, SaveAndYield, ResumeGeneratorGenerator 函数支持
类型转换AsNumberInst, AsInt32Inst, ToPropertyKeyInstJavaScript 的隐式类型转换
浮点优化FUnaryMath, FBinaryMath, FCompare已证明为数值类型时的快速路径

IR 的良构约束(Well-formedness Constraints)是保证编译正确性的关键:

IR 生成由 9 个专门化的 C++ 文件实现(lib/IRGen/ESTreeIRGen-*.cpp),按 JavaScript 语法类别分工:表达式、语句、函数、异常处理、类定义等。

五、优化器

5.1 优化策略

Hermes 的优化器包含 27 个优化 Pass,位于 lib/Optimizer/Scalar/ 目录下。Pass 管理器采用分层固定点迭代的架构:

// 优化 Pass 管理器的执行策略(概念性伪代码)
function optimizeModule(module: Module) {
  // 模块级 Pass
  resolveStaticRequire(module);      // 静态 require 解析

  // 函数级 Pass 循环
  for (const func of module.functions) {
    let changed = true;
    let iterations = 0;
    while (changed && iterations++ < MAX_ITERATIONS) {
      changed = false;
      changed |= typeInference(func);        // 类型推断
      changed |= simplifyMem2Reg(func);      // 栈变量提升
      changed |= scopeElimination(func);     // 作用域消除
      changed |= constantFolding(func);      // 常量折叠
      changed |= deadCodeElimination(func);  // 死代码消除
      changed |= simplifyCFG(func);          // 控制流简化
      changed |= codeMotion(func);           // 循环不变量外提
      changed |= inlining(func);             // 函数内联
    }
  }
}

Hermes 优化器遵循三条设计原则(来自 doc/Optimizer.md):

  1. 单一职责:每个 Pass 只做一件事
  2. 确定性:相同的输入产生相同的输出,迭代顺序可预测
  3. 编译时效率:避免指数复杂度算法,保证编译速度

核心优化 Pass 详解:

TypeInference(类型推断)是最关键的 Pass。JavaScript 是动态类型语言,但通过静态分析可以推断出大量类型信息。当推断出一个变量确定是 Number 类型时,就可以用 FBinaryMath 替换通用的加法指令,跳过类型检查。

ScopeElimination(作用域消除)解决的是闭包变量的堆分配问题。JavaScript 中被闭包捕获的变量需要分配在堆上的 Environment 中。但如果分析表明一个变量虽然在嵌套函数中被引用,但该函数不会逃逸当前作用域,就可以将变量降级为栈变量。

Inlining(函数内联)将小函数的函数体直接嵌入调用点,消除函数调用开销。Hermes 的内联策略偏保守,只在确定内联收益大于代码膨胀成本时才执行。

5.2 React Native 特化

Hermes 的一些优化是专门针对 React Native 代码模式设计的:

六、字节码生成

6.1 字节码格式

Hermes ByteCode(HBC)是最终的执行格式。字节码生成器位于 lib/BCGen/HBC/,核心流程是寄存器分配 + 指令选择

HBC 采用寄存器式字节码格式(而非 Java/.NET 的栈式)。操作数类型包括:

// HBC 操作数类型(来自 BytecodeList.def)
Reg8    // 8-bit 寄存器引用(最多 256 个虚拟寄存器)
Reg32   // 32-bit 寄存器引用(扩展模式)
UInt8   // 8-bit 无符号立即数
UInt16  // 16-bit 无符号立即数
UInt32  // 32-bit 无符号立即数
Addr8   // 8-bit 相对跳转偏移
Addr32  // 32-bit 相对跳转偏移
Imm32   // 32-bit 有符号立即数
Double  // 64-bit IEEE 754 双精度浮点数

HBC 的一个独特设计是双变体指令:每条指令同时有标准版本和 Long 版本。标准版本使用紧凑的 8-bit 操作数,Long 版本使用 32-bit 操作数。大多数函数只需要标准版本,从而显著减小字节码体积。

// 示例:GetById 指令的两个变体
GetById    reg8, reg8, uint8, uint16   // 标准版:4 字节
GetByIdLong reg8, reg8, uint16, uint32  // 长版本:8 字节

// 编译器在寄存器分配后决定使用哪个变体
// 如果所有操作数都在 8-bit 范围内,使用标准版

6.2 压缩策略

对移动端来说,字节码体积直接影响应用包大小和下载时间。Hermes 在多个层面进行压缩:

寄存器分配采用两种策略:

七、虚拟机

7.1 Runtime 架构

Runtime 是 Hermes 虚拟机的核心类,定义在 include/hermes/VM/Runtime.h 中。它是整个执行环境的总管:

// Runtime 的核心组件(概念性描述)
class Runtime {
  GCBase *gc_;            // 垃圾回收器(Hades 或 Malloc)
  IdentifierTable ids_;   // 字符串驻留表(属性名去重)
  SymbolRegistry symbols_; // ES6 Symbol 注册表

  // 预定义对象和原型链
  PinnedHermesValue objectPrototype;
  PinnedHermesValue arrayPrototype;
  PinnedHermesValue functionPrototype;
  // ...

  // 调用栈管理
  StackFrame *currentFrame_;
  StackRuntime stack_;

  // 属性访问缓存(Inline Cache)
  PropertyCacheEntry propertyCache_[];

  // 模块系统
  vector<RuntimeModule*> runtimeModules_;
};

解释器的核心循环实现在 lib/VM/Interpreter.cpp 中,采用字节码线程派发(Threaded Dispatch)机制执行指令。

7.2 NaN-Boxing 编码

Hermes 使用 NaN-Boxing 技术将 JavaScript 值编码在 64 位中。这是一种利用 IEEE 754 浮点数 NaN 空间的巧妙编码:

HermesValue 64-bit 编码(HV64 模式): ┌──────────────┬──────────────────────────────────┐ │ Tag (16-bit) │ Data (48-bit) │ ├──────────────┼──────────────────────────────────┤ │ 正常浮点数 │ IEEE 754 双精度浮点值 │ │ EmptyInvalid │ 0x0000_0000_0000 (空/无效) │ │ UndefinedNull│ 0: undefined, 1: null │ │ BoolSymbol │ Bool 值 或 Symbol ID │ │ NativeValue │ 原生指针 │ │ Str │ StringPrimitive 指针 (48-bit) │ │ BigInt │ BigIntPrimitive 指针 │ │ Object │ JSObject 指针 (48-bit) │ └──────────────┴──────────────────────────────────┘ 关键:双精度 NaN 的范围是 0x7FF0_0000_0000_0001 到 0x7FFF_FFFF_FFFF_FFFF Hermes 用这个空间存储非数值类型的标签和数据

Hermes 还支持 HV32 模式(32-bit 压缩指针),用于 Android 上节省内存。在 HV32 模式下,对象指针被压缩为 32 位偏移量(相对于堆基址),Double 值通过"装箱"存储在堆上。

HV32 的代价

压缩指针节省了内存,但每次访问 double 值都需要一次额外的堆解引用。这是一个经典的空间-时间权衡:Android 设备通常内存更紧张,所以 Hermes 默认在 Android 上使用 HV32。

八、垃圾回收

垃圾回收(GC)是 JavaScript 引擎中最复杂的子系统。Hermes 提供两种 GC 实现,核心是 Hades 并发回收器。

8.1 分代垃圾回收(GenGC)

GenGC 是 Hermes 的早期 GC 实现,基于分代假说:大多数对象在创建后很快变为垃圾。

GenGC 的问题是回收时需要 Stop-The-World 暂停 —— 整个 JavaScript 执行必须停止等待 GC 完成。在老年代回收时,暂停可能达到数十毫秒,导致明显的 UI 卡顿。

8.2 Hades 并发回收器

Hades(lib/VM/gcs/HadesGC.cpp)是 Hermes 的现代 GC,设计目标是最小化暂停时间

Hades 的核心创新是将老年代的标记阶段移到后台线程并发执行。Mutator(执行 JavaScript 的线程)和 GC 线程通过锁和写屏障协调:

Hades GC 的并发执行模型: Mutator 线程(JS 执行) │ GC 后台线程 │ 分配对象到 YG (4MB) │ ───────────────────── │ YG 满 → STW 暂停 │ ├── 标记 YG 中的存活对象 │ ├── 复制存活对象到 OG │ └── 清空 YG │ ─── 恢复执行 ─────────── │ │ 写入 OG 对象的引用 │ ← 写屏障通知 GC 线程 ───────────────────── │ │ OG 使用率 > 75% │ ├── 开始并发标记 │ ├── 遍历 OG 对象图 │ ├── SATB 快照保证一致性 │ └── 标记完成 ───────────────────── │ STW 暂停(极短) │ ├── 完成最终标记 │ └── 开始并发清扫 │ ─── 恢复执行 ─────────── │ │ ├── 回收未标记对象 │ └── 将空间归还自由链表

Hades 的几个关键设计决策:

自由链表分配(而非压缩):传统的标记-压缩 GC 需要移动对象来消除内存碎片,这需要更新所有指向被移动对象的引用 —— 在并发场景中极其复杂。Hades 选择了大小分隔的自由链表(size-segregated free list),按对象大小分桶,分配时直接从对应大小的桶中取空闲空间。这避免了对象移动,简化了并发控制。

SATB(Snapshot-At-The-Beginning)写屏障:并发标记最大的挑战是 Mutator 在标记过程中可能修改对象图。SATB 通过在标记开始时"快照"对象图状态,确保标记阶段看到的是一致的视图。

8.3 写屏障

写屏障是 GC 正确性的基石。每当 Mutator 修改一个堆对象的引用时,写屏障代码会被执行:

// 写屏障的概念性实现
void writeBarrier(GCCell *parent, HermesValue newValue) {
  // 1. SATB 屏障:记录被覆盖的旧值
  //    确保并发标记不会遗漏旧的引用目标
  HermesValue oldValue = parent->slot;
  if (gcPhase == MARKING && isPointer(oldValue)) {
    markQueue.push(oldValue.getPointer());
  }

  // 2. 代际屏障:记录 OG → YG 的引用
  //    YG 回收时不需要扫描整个 OG
  if (isInOldGen(parent) && isInYoungGen(newValue)) {
    cardTable.mark(parent);
  }

  // 3. 实际写入
  parent->slot = newValue;
}

Hermes 的 Card Table 是一个位图,每个位对应堆中 512 字节的区域。当 OG 对象引用了 YG 对象时,对应的 Card 被标记为"脏"。YG 回收时只需要扫描脏 Card 对应的区域,而不是整个 OG。

GC-Safe 编码模式

Hermes 在 C++ 层面设计了完整的 GC 安全编码模式。Handle<T> 是 GC 安全的指针包装器,确保 GC 移动对象时引用自动更新。PseudoHandle<T> 是临时的非安全持有,必须在下一次可能触发 GC 的操作前转换为 Handle。这些模式在 doc/GCSafeCoding.md 中详细文档化。

九、React Native 集成

Hermes 与 React Native 的集成通过 JSI(JavaScript Interface) 实现。JSI 是一个轻量级的 C++ 抽象层,定义了 JavaScript 引擎需要实现的最小接口:

// JSI 接口的核心抽象(简化)
class Runtime {
  // 执行 JavaScript 代码
  Value evaluateJavaScript(Buffer code, string sourceURL);

  // 对象操作
  Object createObject();
  Value getProperty(Object, string name);
  void setProperty(Object, string name, Value);

  // 函数调用
  Value call(Function, Value thisVal, Value[] args);

  // 原生函数注册
  Function createFunctionFromHostFunction(HostFunction);
};

JSI 的设计使得 React Native 可以无缝切换底层引擎(Hermes、V8、JSC),上层代码完全不需要修改。

TurboModules 是 React Native 新架构中的原生模块系统,它通过 JSI 直接调用 C++ 代码,避免了旧架构中 JSON Bridge 的序列化开销。Hermes 对 TurboModules 的支持是原生的 —— 两者共享同一个 JSI 层。

Hermes 的字节码版本与 React Native 版本严格锁定。这意味着 Hermes 编译器生成的字节码只能由同版本的 Runtime 执行 —— 版本不匹配会导致应用崩溃。这是一个务实的工程决策:通过放弃字节码向后兼容性,Hermes 可以在每个版本中自由优化字节码格式。

十、调试基础设施

Hermes 提供了完整的调试支持:

Source Map 支持lib/SourceMap/):字节码到源码的映射。当应用崩溃时,堆栈追踪可以还原到原始 JavaScript 源码的行列号。Source Map 在构建时生成,不打包到最终应用中。

LLDB 调试扩展lldb/):Hermes 为 LLDB 提供了 Python 扩展,支持在 C++ 调试器中直接检查 JavaScript 对象状态。这对引擎开发者调试 GC 问题和 Runtime 崩溃至关重要。

Chrome DevTools 集成:Hermes 实现了 Chrome DevTools Protocol(CDP),允许通过 Chrome 浏览器调试运行在手机上的 React Native 应用。支持断点、单步执行、变量检查、调用栈查看。

字节码中嵌入的调试信息包括:

十一、跨平台构建

Hermes 使用 CMake 作为构建系统,支持多个目标平台:

平台架构构建配置
Androidarm64-v8a, armeabi-v7a, x86_64NDK + CMake toolchain
iOSarm64, arm64 simulatorCocoaPods (hermes-engine.podspec)
macOSx86_64, arm64CMake native
Linuxx86_64CMake native
Windowsx86_64MSVC + CMake
WebAssemblywasm32Emscripten

跨平台构建的关键挑战包括:

十二、性能特征

Hermes 的性能优势主要体现在移动端场景:

启动时间:由于跳过了解析和编译阶段,Hermes 的应用启动时间通常比 V8/JSC 快 30-50%。在低端 Android 设备上,差异更加显著。

内存占用:紧凑的字节码格式(比源码小 50-75%)加上 HV32 压缩指针,使 Hermes 的内存占用通常比其他引擎低 20-30%。

GC 暂停:Hades 的并发标记将 GC 暂停时间从 GenGC 的数十毫秒降低到亚毫秒级。这对保持 60fps 的 UI 流畅度至关重要 —— 16ms 的帧预算中不能有长暂停。

但 Hermes 也有明显的性能弱点:

性能取舍的启示

Hermes 的设计选择告诉我们一个重要的工程原则:不要优化错误的指标。对移动端应用来说,用户感知的"快"来自启动时间和 UI 响应性,而不是 benchmark 中的计算速度。Hermes 通过选择正确的优化目标,在实际用户体验上超越了峰值性能更高的引擎。

十三、总结与思考

Hermes 是一个教科书级的 "约束驱动设计" 案例。它的每一个架构决策都源于对目标场景(移动端 React Native)的深刻理解:

从源码角度来看,Hermes 最值得学习的工程实践包括:

  1. 清晰的编译管道:Parser → IR → Optimizer → BCGen,每一层的输入输出都有明确的接口定义
  2. 指令定义的宏驱动Instrs.defBytecodeList.def 通过 X-Macro 模式从单一定义生成所有相关代码
  3. GC-Safe 编码规范Handle/PseudoHandle/PinnedHermesValue 的类型系统在编译时就能防止大部分 GC 相关 bug
  4. 测试基础设施:Lit 集成测试 + GoogleTest 单元测试 + Test262 规范合规测试
  5. 文档与代码并行doc/ 目录中的设计文档与实现代码保持同步
推荐源码阅读路径

入口一lib/Parser/JSParserImpl.h —— 理解递归下降 Parser 如何处理 JavaScript 的复杂语法。

入口二include/hermes/IR/Instrs.def —— 理解 IR 指令集的设计,以及如何通过宏展开生成代码。

入口三lib/VM/gcs/HadesGC.cpp —— 理解并发 GC 的核心难题:如何在不停止世界的情况下安全回收内存。

入口四include/hermes/VM/Runtime.h —— 理解 VM 的整体结构,所有子系统如何在 Runtime 中协作。