Chrome偷藏了你的JS!V8引擎到底做了什么?

Chrome偷藏了你的JS!V8引擎到底做了什么?

你有没有想过:为什么 JavaScript 能"秒执行"?你写的 console.log('Hello') 到底经历了什么?从 Chrome 偷藏你的代码,到 V8 引擎对你的 JS 做了什么------今天全部揭秘!


原文地址

墨渊书肆/Chrome偷藏了你的JS!V8引擎到底做了什么?


V8 是什么?

JavaScript 引擎

浏览器能执行 JavaScript,全靠 JavaScript 引擎

常见的引擎有:

  • V8 --- Chrome、Node.js、Deno 在用
  • SpiderMonkey --- Firefox 在用
  • JavaScriptCore --- Safari 在用
  • Chakra --- 旧版 Edge 在用

V8 是 Google 开发的高性能引擎,用 C++ 编写,让 JS 执行速度可以媲美编译型语言。

V8 的工作流程

你写的 JS 代码,V8 要做的事情很简单:

md 复制代码
JS代码 → 解析 → 编译 → 执行

但这中间,V8 做了大量偷跑优化

V8 架构演进

时代 架构 说明
早期 Full Codegen → Crankshaft 快速生成机器码,但维护困难
现在 Ignition → TurboFan 字节码+优化编译器,更高效
最新 Ignition + TurboFan + Sparkplug 新增无解释的 baseline JIT

代码是怎么跑起来的?

从 JS 到机器码

你写了一段代码:

javascript 复制代码
function add(a, b) {
  return a + b;
}

console.log(add(1, 2));

V8 拿到这段代码后,经历了这些阶段:

yaml 复制代码
1. 解析(Parser)
   ↓
   把JS代码变成 AST(抽象语法树)
   ↓
2. 解释(Ignition)
   ↓
   编译成字节码,立即执行
   ↓
3. 优化编译(TurboFan)
   ↓
   热代码被编译成高效的机器码
   ↓
4. 执行

Ignition --- 解释器

字节码是什么?

V8 首先用 Ignition 解释器处理代码。

Ignition 会把你的 JS 代码编译成字节码------一种中间代码,比机器码容易生成,但比 JS 容易执行。

javascript 复制代码
// 你写的 JS
function add(a, b) {
  return a + b;
}

对应的字节码(简化版):

python 复制代码
# 字节码类似这样
LdaSmi [1]      # 加载小整数 1
StaA [0]        # 存到 [0] 位置(寄存器)
LdaSmi [2]      # 加载小整数 2
AddA [0]        # 加上 [0] 位置的数
Return           # 返回结果

为什么要转字节码?

直接执行 JS 转字节码再执行
每次都要重新解析 字节码更紧凑
无法优化 可以记录执行信息
启动慢 启动更快

Ignition 不只解释执行,还会记录信息------哪些函数被调用多次、参数类型是什么。这些信息给后续优化用。

Ignition 的执行反馈

javascript 复制代码
function add(a, b) {
  return a + b;
}

add(1, 2);      // 第1次:记录类型
add(3, 4);      // 第2次:类型一致,继续记录
add("x", "y");  // 第3次:类型变了!记录下来

Ignition 维护一个 Feedback Vector(反馈向量),记录每段代码的类型信息。


TurboFan --- 优化编译器

JIT 是什么?

JIT(Just-In-Time)= 即时编译。

不是提前编译好,而是一边执行一边编译。执行次数多的代码,会被更高效的机器码替代。

TurboFan 优化流程

TurboFan 不是直接生成最优机器码,而是层层优化:

yaml 复制代码
字节码 + 执行反馈
   ↓
Sea of Nodes(中间表示)
   ↓ 优化 Pass 1: 类型推导
   ↓ 优化 Pass 2: 内联
   ↓ 优化 Pass 3: 环路优化
   ↓ 优化 Pass 4: 寄存器分配
   ↓
机器码

热代码检测

V8 有一套"热点检测"机制:

javascript 复制代码
function add(a, b) {
  return a + b;
}

// 这个函数被调用了10000次
for (let i = 0; i < 10000; i++) {
  add(1, 2);
}
yaml 复制代码
调用次数 < 1000
↓
Ignition 解释器执行(字节码)

调用次数 > 1000
↓
TurboFan 优化编译(机器码)

优化与反优化

TurboFan 很聪明,但也有"翻车"的时候:

javascript 复制代码
function add(a, b) {
  return a + b;
}

// 前1000次调用,参数都是整数
for (let i = 0; i < 1000; i++) {
  add(1, 2);  // TurboFan 优化:整数加法
}

// 第1001次,参数变成字符串
add("hello", "world");  // 反优化!退回字节码

TurboFan 发现类型变了,会反优化(Deoptimization),退回字节码。

常见的优化场景

javascript 复制代码
// ✅ 好优化:类型稳定
function length(arr) {
  return arr.length;  // 数组 length 是稳定的
}
length([1, 2, 3]);
length([4, 5]);

// ❌ 难优化:类型不稳定
function getX(obj) {
  return obj.x;  // obj 可能是任意类型
}
getX({ x: 1 });
getX("string");  // 字符串没有 x 属性!

隐藏类 --- 快速属性访问

对象属性查找

JS 里访问对象属性很快,这要归功于隐藏类 (Hidden Class),也叫 ShapesMaps

javascript 复制代码
const person = { name: 'Tom', age: 18 };

V8 内部会为这个对象创建一个隐藏类:

yaml 复制代码
隐藏类 HC0
├── name: offset 0
└── age: offset 1

属性访问加速原理

当你访问 person.name 时:

javascript 复制代码
// 幕后发生的事情
person.name
  → 通过隐藏类 HC0
  → 直接定位到 offset 0
  → 拿到值 "Tom"

就像图书馆的书有固定编号(隐藏类),管理员知道每本书在哪个书架第几格。

隐藏类转换

对象属性改变时,会产生新的隐藏类:

javascript 复制代码
const obj = { x: 1 };
//   ↓ 添加 y
obj.y = 2;
//   ↓ 修改 x
obj.x = 10;
yaml 复制代码
HC0: { x: 1 }
  ↓ 添加 y 属性
HC1: { x: 1, y: 2 }
  ↓ 修改 x 属性(值变化不改变结构)
HC1(不变)

属性顺序很重要!

javascript 复制代码
// 好:属性顺序一致 → 共享同一个隐藏类
const p1 = { x: 1, y: 2 };
const p2 = { x: 3, y: 4 };

// 差:属性顺序不一致 → 产生多个隐藏类
const p3 = { y: 1, x: 2 };  // 新建 HC1!

多态与全态

javascript 复制代码
// 单态(Monomorphic):一种隐藏类,最快
function getX(obj) { return obj.x; }
getX({ x: 1 });      // HC0
getX({ x: 2 });      // 还是 HC0,命中缓存

// 多态(Polymorphic):2-4种隐藏类,较慢
function getX(obj) { return obj.x; }
getX({ x: 1, a: 0 });    // HC0
getX({ x: 2, b: 0 });    // HC1

// 全态(Megamorphic):5+种隐藏类,最慢
function getX(obj) { return obj.x; }
getX({ ... });  // 每次都是新结构

内联缓存 --- 加速函数调用

函数调用有多慢?

函数调用看起来简单:

javascript 复制代码
function getName(user) {
  return user.name;
}

const user = { name: 'Tom' };
getName(user);

但每次调用,V8 都要查找 user.name 在哪里。

内联缓存的原理

V8 第一次执行 getName(user) 时:

yaml 复制代码
第1次调用:
1. 查找 user 的隐藏类 → HC0
2. 查找 name 属性在 HC0 的位置 → offset 0
3. 返回结果
4. 记录:HC0 的对象调用这个函数,返回 offset 0

之后调用同样的函数,直接跳过查找

yaml 复制代码
第2次调用:
1. 检查隐藏类是 HC0 ✓
2. 直接用记录的 offset 0
3. 返回结果

这就是内联缓存(Inline Cache)------把查找结果"缓存"起来。

IC 的类型状态

yaml 复制代码
Uncached → Monomorphic → Polymorphic(2-4) → Megamorphic(5+)
   ↓            ↓              ↓              ↓
 每次查     命中缓存       部分命中         全局查表

垃圾回收 --- 内存管理

什么是垃圾?

程序里不再使用的对象就是"垃圾":

javascript 复制代码
function createUser() {
  const user = { name: 'Tom' };
  return user.name;  // user 对象还在用
}  // 但 user 变量没了

createUser();
// 之后再也访问不到这个 { name: 'Tom' } 对象了
// 它就成了"垃圾"

V8 的内存布局

yaml 复制代码
┌─────────────────────────────┐
│          新生代             │  ← 新对象
│    (New Space / Semi-Space) │
├─────────────────────────────┤
│          老生代             │  ← 存活久的对象
│    (Old Space)              │
├─────────────────────────────┤
│        大对象区              │  ← 无法放入其他区的对象
│    (Large Object Space)    │
├─────────────────────────────┤
│        代码区                │  ← JIT 编译后的机器码
│    (Code Space)            │
├─────────────────────────────┤
│        Cell / Map           │  ← 特殊对象
│    (Cell / Map Space)       │
└─────────────────────────────┘

V8 的垃圾回收策略

V8 采用分代回收

代际 对象来源 回收频率 算法
新生代 新创建的对象 频繁 Scavenge(复制)
老生代 经历一次 GC 仍存活 较少 Mark-Sweep-Compact

新生代:Scavenge 算法

新生代内存分两半:FromTo

css 复制代码
┌─────────────────┬─────────────────┐
│      From       │       To        │
│   (使用中)     │   (空闲)       │
└─────────────────┴─────────────────┘

1. From 满了,存活对象复制到 To
2. From 清空
3. From 和 To 交换

晋升:经历两次 Scavenge 仍存活的对象,会进入老生代。

老生代:Mark-Sweep-Compact

步骤1:标记(Mark)

yaml 复制代码
遍历所有根对象(全局变量、栈上变量)
    ↓
标记能访问到的对象为"存活"
    ↓
没被标记的就是垃圾

步骤2:清除(Sweep)

yaml 复制代码
回收没有标记的对象的内存

步骤3:压缩(Compact)

yaml 复制代码
存活对象移动到一起
↓
解决内存碎片问题

增量 GC

为了避免长时间停顿(Stop-The-World),V8 使用增量标记:

yaml 复制代码
传统 GC:
████████████████████████████  100% 停顿
     执行时间 ←────────────────→

增量 GC:
███    ████    ███    ██
  ↓      ↓      ↓      ↓
执行  执行  执行  执行

Orinoco --- 并行与并发 GC

现代 V8 使用更先进的 GC 算法:

技术 说明 效果
并行 GC GC 多线程并行执行 充分利用多核 CPU
增量 GC GC 分多次小步执行 减少停顿时间
并发 GC GC 与 JS 执行同时进行 几乎无停顿

深入了解 V8 🔬

V8 执行流程全图

yaml 复制代码
JS代码
   ↓ Parser
AST(抽象语法树)
   ↓ Ignition
字节码 + Feedback Vector(反馈向量)
   ↓ (热代码触发)
TurboFan
   ↓
优化机器码
   ↓ (类型不稳定)
反优化 → 退回字节码

为什么 V8 这么快?

优化手段 作用
JIT 即时编译 热代码用机器码执行
隐藏类 对象属性快速访问
内联缓存 函数调用加速
分代回收 高效内存管理
懒解析 延迟解析,只解析用到的
并行 GC 多核加速垃圾回收

Sparkplug --- 无解释的 Baseline JIT

V8 最近引入了 Sparkplug,一个超快的 baseline JIT:

yaml 复制代码
之前:JS → Ignition 字节码 → TurboFan 机器码
现在:JS → Ignition 字节码 → Sparkplug 机器码 → TurboFan 优化机器码

Sparkplug 不做任何优化,直接把字节码转成机器码,比 Ignition 快 2-5 倍。

TurboFan 优化的代码例子

javascript 复制代码
// 优化前:字节码执行
function sum(arr) {
  let total = 0;
  for (let i = 0; i < arr.length; i++) {
    total += arr[i];
  }
  return total;
}

// 优化后:TurboFan 可能生成的机器码
// 1. 使用寄存器代替变量
// 2. 循环展开(Loop Unrolling)
// 3. 预取数据到 CPU 缓存

编写高性能 JS

javascript 复制代码
// ✅ 好:保持属性类型一致
const p1 = { x: 1, y: 2 };
const p2 = { x: 3, y: 4 };

// ✅ 好:避免类型变化
function add(a, b) {
  return a + b;
}
add(1, 2);       // 都是整数
add(3.14, 2.86); // 都是浮点数

// ❌ 差:属性顺序不一致
const a = { x: 1, y: 2 };
const b = { y: 1, x: 2 };  // 新建隐藏类!

// ❌ 差:类型乱变
function example(x) {
  return x.value;  // x 可能是对象,可能是 undefined
}

// ✅ 好:使用固定形状的对象
const cache = {};
for (let i = 0; i < 1000; i++) {
  cache.key = i;  // 每次都用相同的 key
}

V8 性能陷阱

陷阱 说明 解决方案
隐藏类爆炸 对象结构不一致 保持属性顺序一致
类型不稳定 参数类型经常变化 使用多态函数时要小心
内存泄漏 闭包引用大量对象 及时解除引用
大对象 大数组、大对象放新生代 手动管理或拆分

总结

概念 作用 比喻
Ignition 解释器,生成字节码 + 记录反馈 同声传译先听懂意思
TurboFan 优化编译器,生成高效机器码 翻译稿润色升级
JIT 即时编译,热代码加速 多次练习后越说越溜
隐藏类 快速属性访问 图书馆编号系统
内联缓存 函数调用加速 记住常走的路
分代回收 高效内存管理 新书放前台,旧书放仓库
Sparkplug 超快 baseline JIT 不用练习,直接上岗

写在最后

现在你知道了:

  • V8 不是直接执行 JS,而是经过 Parser → Ignition → TurboFan
  • JIT 让热代码越来越快,但类型变化会导致反优化
  • 隐藏类和内联缓存,是 JS 快的秘密
  • 写代码时保持类型一致,能帮助 V8 优化
  • 新生代用复制算法,老生代用标记清除

下次有人说"JS 慢",你可以理直气壮地说:你了解 V8 吗?

相关推荐
持续前行4 小时前
通过 npm 下载node_modules 某个依赖 ;例如 下载 @rollup/rollup-linux-arm64-gnu
前端·javascript·vue.js
chenyingjian4 小时前
鸿蒙|能力特性-统一文件预览
前端·harmonyos
毛骗导演4 小时前
OpenClaw 沙箱执行系统深度解析:一条 exec 命令背后的安全长城
前端·架构
天才聪4 小时前
鸿蒙开发vs前端开发1-父子组件传值
前端
卡尔特斯4 小时前
Android Studio 代理配置指南
android·前端·android studio
李剑一4 小时前
同样做缩略图,为什么别人又快又稳?踩过无数坑后,我总结出前端缩略图实战指南
前端·vue.js
Jolyne_4 小时前
Taro样式重构记录
前端
恋猫de小郭5 小时前
Google 开源大模型 Gemma4 怎么选,本地跑的话需要什么条件?
前端·人工智能·ai编程
文心快码BaiduComate5 小时前
Comate搭载GLM-5.1:长程8H,对齐Opus 4.6
前端·后端·架构
熊猫钓鱼>_>5 小时前
AI驱动的Web应用智能化:WebMCP、WebSkills与WebAgent的融合实践
前端·人工智能·ai·skill·webagent·webmcp·webskills