🚀JS 为什么能跑这么快?一文把 V8 “翻译官 + 加速器” 机制讲透(AST / 字节码 / JIT / 去优化)
你的 JS 到底怎么跑起来的?一文看懂 V8:从源码到机器码的“流水线”(含图解)
写下
console.log('hi')的那一刻,CPU 其实完全看不懂。
真正让 JS “跑起来”的,是 JavaScript 引擎——尤其是 Chrome/Node.js 背后的 V8。
这篇文章用一条清晰的流水线,把 V8 的核心机制讲透:Parse → AST → Ignition(字节码) → TurboFan(机器码) → Deopt(去优化回退) 。
文章推荐:
代码10倍提速!吃透底层架构就是如此简单-腾讯云开发者社区-腾讯云
先建立直觉:V8 是一条“翻译+加速”的流水线
可以把 V8 想象成一个“会学习的翻译官”:
- 第一目标:让代码尽快跑起来(启动快)
- 第二目标:把经常跑的代码越跑越快(热点优化)
- 第三目标:发现假设错了就回退重来(去优化 Deopt)
接下来所有细节,都围绕这三句话展开。
01|为什么 CPU 才是最终执行者
CPU(中央处理器)执行的是机器语言——一串二进制指令。它不认识 JavaScript、也不认识“高级语言”的语法糖。
所以:CPU 是“执行者”,V8 是“翻译官 + 加速器”。
![]()
再看一张更直观的图:代码最终一定要落到 CPU 可执行的机器码上。
![]()
02|JavaScript 引擎在浏览器里处在什么位置
浏览器内核并不是“只有渲染”,它通常至少包含两大块:
- 渲染相关:HTML/CSS 解析、布局、绘制
- 脚本相关:解析并执行 JavaScript
以 WebKit 举例:它可以拆成 WebCore 和 JavaScriptCore 两部分(JS 引擎就是内核的一部分)。
![]()
03|V8 全流程:从源码到机器码
把 V8 的执行流程浓缩成 6 步,会非常清晰:
- Parse(解析) :源码 → AST(抽象语法树),并采用 Lazy Parsing(函数即将执行时才完整解析)
- Ignition(解释器) :AST → 字节码 Bytecode
- 执行字节码:先跑起来,并收集运行信息(类型、分支、调用频率…)
- TurboFan(优化编译器) :热点代码 → 优化后的机器码
- Deopt(去优化) :假设不成立(常见是类型变化)→ 回退到字节码
- 机器码执行:最终交给 CPU
用一张图把这条流水线钉死在脑子里:
![]()
同时,AST 长什么样?大概是这种结构化树形表示:
![]()
04|Parse 细节:词法分析、语法分析与 AST
很多人卡在“Parse 解析”这一步,原因是:概念名词多,但直觉不够。
4.1 词法分析:把代码拆成 token(最小语法单元)
可以理解为“拆词”——把一段 JS 源码拆成一个个最小的记号(token):
- 关键字
function - 标识符
sayHi - 运算符
=,+ - 标点符号
(),{},; - 字面量
"Hi "
4.2 语法分析:把 token 重新组装成树(AST)
可以理解为“造句”——把 token 按语法规则组装成结构化表达,这棵树就是 AST。
一个好记的口诀:
先词后语:先把“单词”拆出来,再把“语句结构”搭起来。
05|为什么要保留“字节码”这一层
直觉上会觉得:少一层转换就更快,那为什么不直接 AST → 机器码?
因为工程里真正的目标不是“某一步最快”,而是“整体更快、更稳、更可控”。保留字节码主要带来:
- 跨平台:字节码不绑定某一种 CPU 指令集
- 优化更聪明:先跑字节码,收集运行数据,再决定怎么生成更优机器码
- 更安全、更可控:更容易做隔离、策略、内存管理
- 更容易调试:断点/单步在字节码层更容易实现
配合这张图理解,会很顺:
![]()
06|架构拆解:Parse / Ignition / TurboFan 各做什么
用“岗位职责”来记:
- Parse:把 JS 代码变成 AST(解释器不直接认识 JS 源码)
- Ignition:把 AST 变成字节码并执行,同时收集 TurboFan 需要的运行信息(比如类型信息)
- TurboFan:把热点字节码编译成更快的机器码(并持续迭代优化)
这里有一个非常关键的运行规律:
热点函数会被优化,但类型变化等情况会触发去优化回退。
07|预解析 vs 全量解析:Lazy Parsing 为什么能让启动更快
V8 并不会“上来就把一切都解析得巨细无遗”,它会做取舍:
7.1 预解析(Pre-parsing)
- 目标:快速扫描,提取结构信息(变量/函数声明等)
- 特点:不深挖函数体内部逻辑 → 更快
7.2 全量解析(Full parsing)
- 目标:把函数体、表达式、语句细节全部建出来
- 特点:AST 更完整 → 便于后续生成字节码与优化
因此,“函数没执行会不会生成 AST?”更准确的回答是:
- 会生成一个简化的结构架子(预解析)
- 真要执行之前,会补齐为完整 AST(全量解析)
08|走一遍官方图:token、AST、字节码到底怎么来的
先准备一段模板代码:
name = "XiaoWu"
console.log(name)
function sayHi(name) {
console.log("Hi " + name)
}
sayHi(name)
8.1 官方流程图:从输入到字节码
这张图非常经典,建议收藏:
![]()
按图理解就是:
- Scanner:扫描字符流 → 生成 tokens
- PreParser:做预解析(快速判断结构)
- Parser:构建 AST
- Bytecode:AST → 字节码
8.2 token 长什么样(词法分析结果)
下面是典型 token 形态(摘取关键类型,方便理解):
Token(type='Keyword', value='const') // 关键字
Token(type='Identifier', value='name') // 标识符
Token(type='Operator', value='=') // 运算符
Token(type='StringLiteral', value='"coderwhy and XiaoYu"') // 字符串字面量
Token(type='Punctuation', value=';') // 标点符号
Token(type='Identifier', value='console')
Token(type='Punctuation', value='.')
Token(type='Identifier', value='log')
Token(type='Punctuation', value='(')
Token(type='Identifier', value='name')
Token(type='Punctuation', value=')')
Token(type='Punctuation', value=';')
8.3 语法分析:预解析如何参与
这张图专门解释“预解析/解析”的关系:
![]()
09|热点优化与去优化:为什么“有时突然变慢”
V8 会把被频繁执行的函数标记为 热点函数,然后交给 TurboFan 编译为更快的机器码。
但注意:优化是有前提假设的。最常见的假设就是“类型稳定”。
来看这个例子:
function sum (num1,num2){
return num1 + num2
}
// 多次调用 -> 可能成为热点函数 -> 被优化
sum(20,20)
sum(20,20)
// 类型突然变化 -> 之前的机器码假设不成立 -> 去优化回退
sum('xiaoyu','coderwhy')
发生了什么?
- 前两次传入
number,优化器可能会假设“这里一直是 number 加法” - 第三次突然变成
string拼接,机器码可能无法正确处理 → 回退到字节码重新收集信息,再决定是否重新优化
这就是性能“抖一下”的根源之一:Deopt(去优化) 。
10|字节码与机器码(了解即可):JIT 到底做了什么
机器码的生成通常依赖 JIT(Just-In-Time Compilation,即时编译) :
- 把字节码转换成本地机器码
- 把结果缓存起来
- 后续执行直接复用缓存的机器码(更快)
TurboFan 作为优化编译器,会基于 IR(中间表示)做多层优化(类型、内联、控制流等):
![]()
同时,字节码到机器码的过程中,会存在不同优化策略:
![]()
这里还有两张配图(保持原样保留):
![]()
![]()
结尾:把知识用起来
理解 V8 的意义,不是为了背名词,而是为了形成“性能直觉”:
- 让热点函数更容易被优化:参数类型尽量稳定
- 减少去优化回退:避免同一段热点路径里频繁出现类型漂移
- 理解启动性能:Lazy Parsing 的策略决定了“先跑起来”的快慢