Skip to content
LLuka Piplica
browser-engineeringchromiumv8-engineweb-performancenetwork-protocolsweb-history

浏览器战争剖析:从互联网泡沫到 Chromium 的霸权

从系统级视角拆解现代 Web 浏览器:Chrome 多进程模型、V8 JIT 管线、渲染路径、HTTP/3 演进以及 WebAssembly 在原生性能中的作用。

L

Luka Piplica

7 分钟阅读
Windows 95 拨号连接进度对话框动画,显示拨号电话与计算机之间移动的连接点,代表早期 Web 时代。

1995 年,“浏览器”还是一款职责单一的软件:通过 HTTP 获取 HTML 文件,并将文本和图像绘制到画布上。三十年后的今天,同类软件运行着 Figma 的实时协作画布,解码 4K 视频,在 WebGL/WebGPU 游戏中模拟物理效果,并在单次会话中执行数 GB 的 JavaScript —— 其性能甚至常常超越它所替代的原生桌面应用。这种巨变绝非偶然。它的发生,是因为少数几个为了争夺市场份额的工程团队,被迫去解决操作系统级别的难题:进程隔离、即时编译(JIT)、GPU 加速合成以及传输层协议的重新设计。

本文将从系统级视角深入拆解现代浏览器 —— 尤其是如今驱动 Chrome、Edge、Opera、Brave 和 Arc 的 Chromium 谱系 —— 如何在实际意义上演变为一个运行在操作系统之上的“操作系统”。

1. 从 Netscape 到 Chromium:浏览器演变为操作系统

第一次与第二次浏览器战争

第一次浏览器战争(1995–2001 年)是 Netscape Navigator 与微软 Internet Explorer 之间的对决。微软决定将 IE 直接与 Windows 捆绑 —— 免费、预装且与操作系统 Shell 深度集成 —— 这种分发优势是 Netscape 基于许可证授权的商业模式无法比拟的。到 2002 年,IE 占据了超过 90% 的市场份额,Netscape 的代码库被作为 Mozilla 项目开源,而在 IE6 停滞不前的渲染引擎统治下,Web 创新实际上停滞了整整五年。

第二次浏览器战争(2004–2008 年)则是基于 Gecko 渲染引擎的 Mozilla Firefox 发起的反击。Firefox 在标准兼容性、标签页浏览和扩展性方面重新引入了竞争压力,从自满的 IE 手中夺回了相当一部分市场份额。

随后,在 2008 年 9 月,Google 推出了 Chrome。它不仅带来了一个更快的 JavaScript 引擎(下文将介绍的 V8),更从根本上带来了一种全新的进程模型,彻底重塑了浏览器“标签页”的概念。Chrome 的渲染引擎最初是 Apple WebKit(本身衍生自 KHTML)的一个分支;2013 年,Google 将 WebKit 分叉为 Blink,此后 Blink 便成为了 Web 事实上的底层基石。Microsoft Edge 在 2020 年放弃了自研的 EdgeHTML 引擎,转而采用 Blink。其结果是一种被称为 Chromium 霸权 的近乎垄断的局面:如今绝大多数面向消费者的浏览器都共享同一个渲染核心,主要区别仅在于 UI 界面(UI chrome)和默认隐私设置。

从文档查看器到应用运行时

随着 Web 从静态文档转向应用程序,对浏览器提出的技术需求呈数量级增长:AJAX(2005 年)将页面转变为无需刷新即可获取数据的有状态客户端;<canvas> 以及后来的 WebGL API 将浏览器转变为可被 GPU 访问的渲染目标;Node.js 证明了 JavaScript 可以在服务端大规模运行,这反哺了对浏览器 JS 引擎的投资;而渐进式 Web 应用(PWA)则完全模糊了“网站”与“已安装应用”之间的界限。

一个能够同时在不同标签页中运行 Google Docs、多人 3D 游戏和实时视频通话的浏览器,绝非仅仅是一个文档查看器。它是一个多租户运行时(multi-tenant runtime),需要有与之相匹配的架构。

Chrome 的架构胜局:全面多进程化

Chrome 诞生之前的浏览器很大程度上是单进程、多线程的应用。每个标签页、每个扩展程序以及浏览器自身的 UI 都共享同一个地址空间。单个格式错误的 <script> 标签、有缺陷的插件或渲染器 Bug 都会导致所有打开的标签页同时崩溃 —— 并且由于所有内容共享内存,一个标签页中的渲染 Bug 就可能成为侵入另一个标签页数据的潜在安全漏洞。

Chrome 的底层架构决策是将浏览器拆分为相互协作的操作系统级进程:

  • 主进程 / 浏览器进程(Browser Process) —— 拥有特权的编排者。掌控 UI(地址栏、书签、标签栏),管理磁盘缓存,并且是唯一拥有对网络和文件系统无限制访问权限的进程。
  • 渲染进程(Renderer Process) —— 每个标签页/站点分配一个(或多个),运行 Blink 和 V8。这些进程被有意地进行沙盒化:渲染进程无法直接触及文件系统,无法发起任意系统调用(syscalls),任何需要特权的操作都必须通过进程间通信(IPC)向主进程请求。
  • GPU 进程(GPU Process) —— 拥有图形上下文并执行合成的单个共享进程,将脆弱且易崩溃的 GPU 驱动程序代码与主进程和渲染进程隔离开来。
  • 网络进程、实用工具进程、扩展程序进程 —— 对解析、音频和第三方扩展代码进行更进一步的解耦拆分。

图 1:Chromium 多进程设计的架构图

图 1:Chromium 的多进程架构。主浏览器进程统筹 UI 和网络操作,同时通过进程间通信(IPC)通道与隔离的沙盒化渲染进程通信,以强制执行安全边界。

最直观立竿见影的优势在于崩溃隔离:单个标签页崩溃 —— 即那张恶名昭彰的“呜呼!崩溃啦!”(Aw, Snap!)页面 —— 不再会导致窗口中打开的其他二十个标签页一并受殃。更深层次的胜利在于安全性:由于渲染进程经过沙盒化处理并被视为不可信,Blink HTML 解析器中的远程代码执行 Bug 不再会直接向攻击者双手奉上你整台机器的权限;给到他们的仅仅是一个几乎没有任何特权的沙盒化进程,攻击者必须再找到第二个独立的沙盒逃逸 Bug 才能造成真正的破坏。

站点隔离:Spectre 漏洞后的加固

仅凭多进程架构还远远不够。起初,Chrome 按照进程对标签页进行分组的方式较为宽松(通常按标签页,而非严格按源 origin),这意味着内嵌在 trusted-bank.example 中的 evil.example 的 <iframe> 仍可能与父页面共享同一个渲染进程 —— 从而共享同一个地址空间。

2018 年披露的 Spectre 幽灵推测执行侧信道漏洞彻底改变了这一局面。Spectre 表明,恶意脚本可以通过利用 CPU 推测执行来读取其自身进程内的任意内存 —— 这意味着如果受信任的代码与敌意代码共享同一个进程,仅靠进程级沙盒已不再足够。Chrome 的应对方案是站点隔离(Site Isolation):每个独立的站点(大致定义为协议 scheme + 可注册域名 registrable domain)都会获得专用的渲染进程,即便对于嵌套在其他页面内部的跨源 iframe 也是如此。页面及其 iframe 之间的跨进程通信完全通过 IPC 进行中介,因此不存在可供类 Spectre 攻击读取利用的共享内存。

2. V8 引擎:让动态语言飞跃提速

1995 年,JavaScript 在短短十天内被设计出来,最初仅作为一种轻量级脚本胶水语言。它具备动态类型、垃圾回收,并允许对象形状在运行时动态改变 —— 这些特性无一有利于高速执行。Google 推出的 V8 引擎(首次随 Chrome 发布)正是 JavaScript 如今能在实际工作负载中与静态编译语言一高下下的根本原因。V8 通过一个三级管线来实现这一点:一个快速启动的解释器、一个填补延迟空隙的中级编译器,以及一个顶级的推测性优化编译器。

图 2:V8 解析与字节码编译管线

图 2:V8 中的解析与字节码生成管线。JavaScript 源代码被解析为抽象语法树(AST),随后由 Ignition 解释器将其直接编译为基于寄存器的字节码以备执行。

Ignition:字节码解释器

当 V8 首次接收到 JavaScript 代码时,它不会直接编译为机器码。解析器会首先构建一个抽象语法树(AST),接着 V8 的解释器 Ignition 将该 AST 编译为紧凑的、基于寄存器的字节码。字节码远比同等的机器码要小得多,且几乎可以瞬间生成 —— 这对于页面加载性能(用户正等待首次绘制)至关重要。

Ignition 直接执行这些字节码,最关键的是,它还会在运行过程中对执行情况进行分析(profiling):哪些函数被频繁调用、哪些调用点具有稳定的参数类型、哪些分支是热点(hot)。这些分析数据构成了驱动上层管线的燃料。

Maglev:填补延迟空隙

Ignition 的字节码能让代码几乎瞬间运行起来,但解释器在每次执行时都必须支付单条指令的分派开销 —— 对于调用两次的函数这无伤大雅,但对于在热点循环中调用数万次的函数则是毁灭性的。显而易见的解法是直接编译为原生机器码。然而 V8 的顶级编译器 TurboFan 被有意设计得非常重型:它会构建函数的完整图表示(graph representation),并通过漫长且多次迭代的优化 pass 管线 —— 包括内联(inlining)、逃逸分析(escape analysis)、范围分析(range analysis)、加载/存储消除(load/store elimination) —— 这些虽然能生成极佳的机器码,但付出的代价是可观的编译时间与内存。如果在某个函数刚刚表现出“温热(warm)”迹象时就对其触发 TurboFan,将会浪费大量 CPU 预算去编译那些在页面跳转前可能仅再执行几百次的代码。

Maglev 正是为了填补这一空隙而生。作为 V8 引入的中级 JIT 编译器,它在管线中直接介于 Ignition 与 TurboFan 之间。Maglev 不会构建并反复重写重型图,而是直接在 Ignition 字节码上执行单趟编译(single-pass compilation),构建一个**静态单赋值(SSA)**表示 —— 其中每个变量恰好被赋值一次,从而简化了数据流分析 —— 并在单个线性 pass 中将其直接降级(lowering)为原生机器码,跳过了 TurboFan 所依赖的多轮图重写。其输出虽然不像 TurboFan 那样经过极其激进的优化,但它是货真价实的原生机器码,编译耗时仅为前者的一小部分,且运行速度轻松碾压解释执行的字节码。

这赋予了 V8 一个渐进式的阶梯提速过程:Ignition 从第一次调用开始以几乎为零的编译延迟执行函数;一旦函数表现出“温热”,V8 便将其升阶至 Maglev 换取廉价且快速的原生代码;只有当函数证明自己确实且持续处于“热点(hot)”状态时,V8 才会付出大得多的投资将其升阶至 TurboFan 以实现最大化优化。

TurboFan:优化编译器

一旦某个函数证明自己不仅仅是温热,而是持续处于热点状态 —— 其被调用或循环执行的次数足以让 TurboFan 较重的编译成本带来翻倍的回报 —— V8 就会将其移交给顶级优化 JIT 编译器 TurboFan。TurboFan 结合来自 Ignition 和 Maglev 积累的类型反馈,生成 V8 所能产生的最激进优化的原生机器码,并针对其具体观测到的类型进行专门化处理 —— 例如,假设函数的参数始终是一个小整数(small integer),从而消除完全通用实现所必需的通用类型检查以及装箱/拆箱开销。

从根本上说,这是一种推测性优化(speculative optimization):TurboFan 赌过去的类型行为可以预测未来的类型行为。如果这项赌注随后被打破 —— 例如一个始终接收整数的函数突然接收了一个字符串 —— V8 就必须执行反优化(deoptimize),抛弃专门化的机器码并退回到较低层级(Maglev 的代码,或最终退回 Ignition 的字节码),之后可能会基于更宽泛的假设重新进行优化。频繁的反优化是 JavaScript 性能退化中最常见、也最隐蔽的原因之一。

隐藏类:伪造静态结构

核心问题在于:在静态类型语言中,编译器在编译时确切知道每个对象属性在内存中的位置,因此 obj.x 会被编译为固定内存偏移量的读取。而在 JavaScript 中,对象是动态字典 —— 属性可以随时添加或删除 —— 因此朴素的实现需要为每一次属性访问都进行哈希表查找,在大规模场景下这极为缓慢。

V8 的解决方案是隐藏类(Hidden Classes,内部称为 Map,注意不要与 JS 的 Map 类型混淆)。每个 JavaScript 对象内部都秘密关联着一个描述其当前“形状(shape)”的隐藏类:包含哪些属性,以及每个属性位于哪个偏移量。当两个对象以相同的顺序添加属性进行构造时,V8 会识别出它们共享相同的形状,并为它们分配同一个隐藏类 —— 这意味属性访问现在可以像静态语言一样编译为直接的偏移量读取。

一旦对象的形状发生改变 —— 添加了新属性或删除了属性 —— V8 就必须将其迁移(transition)到一个新的隐藏类。如果两个构造函数构建了逻辑上相似的对象,但以不同的顺序分配属性,V8 会为它们生成完全不同的隐藏类迁移链,从而使这项优化失效:

hidden_classes.js
1// 这两个对象最终拥有不同的隐藏类,
2// 即使它们最终拥有“相同”的属性,
3// 因为属性插入的“顺序”不同。
4
5function PointGood(x, y) {
6 this.x = x; // 迁移:{} -> {x}
7 this.y = y; // 迁移:{x} -> {x, y}
8}
9
10function PointBad(x, y) {
11 this.y = y; // 迁移:{} -> {y}
12 this.x = x; // 迁移:{y} -> {y, x} <-- 不同的迁移链!
13}
14
15const a = new PointGood(1, 2);
16const b = new PointBad(1, 2);
17
18// a 和 b 在内部携带着两个不同的隐藏类,
19// 即使 Object.keys(a) 和 Object.keys(b) 看起来完全相同。
20// 任何在混合了这两种形状的数组上运行的代码,
21// 都会迫使 V8 的内联缓存进入更慢的“多态”状态。

内联缓存:记住最近见过的形状

隐藏类解决了属性查找如何发生;而**内联缓存(Inline Caching, IC)**则解决了在你代码的特定位置查找发生得有多快。每个属性访问点(即“调用点”,例如特定行 obj.x)都维护着一个小型缓存,记录着上次见到的隐藏类以及对应的内存偏移量。当下一次执行同一行代码时,V8 会检查:“这个对象的隐藏类是否与上次相同?”如果是,它将完全跳过查找过程,直接跳转到缓存的偏移量。

内联缓存会经历三种状态:

  • 单态(Monomorphic) —— 调用点只见过一种隐藏类。这是速度最快的执行路径。
  • 多态(Polymorphic) —— 调用点见过少数几种不同的隐藏类(V8 会缓存几种并依次检查)。速度依然相当快。
  • 超态(Megamorphic) —— 调用点见过的不同形状过多,无法有效缓存。V8 放弃 IC 快速路径,退回到通用的、慢得多的属性查找方式。

这正是为何库和代码风格指南强烈建议保持对象构造的一致性(固定的属性集、一致的插入顺序、避免使用 delete):这绝非迷信,而是为了将内联缓存维持在单态。

3. 关键渲染路径:从字节到像素

一旦 V8 执行完脚本,浏览器仍需将文档及样式转换为物理显示屏上的实际图像,理想情况下为每秒 60(或 120)帧。这一管线 —— 关键渲染路径(Critical Rendering Path) —— 正是网络字节转化为像素的地方。

图 3:关键渲染路径执行管线

图 3:关键渲染路径管线。浏览器将 HTML 和 CSS 解析为 DOM 和 CSSOM 树,将它们合并为渲染树,并执行布局、绘制和合成 Pass,从而在屏幕上渲染最终帧。

DOM 与 CSSOM 的构建

随着 HTML 字节流的到达,Blink 的 HTML 解析器会对其进行增量分词(tokenize),并构建 **DOM(文档对象模型,Document Object Model)树 —— 这是每个元素和文本节点的动态结构化表示。解析在设计上是流式且增量的:浏览器无需等待整个文档加载完毕才开始构建树,这也是为什么预加载扫描器(preload scanner)**会跑在主解析器之前,提前识别 <img>、<link> 和 <script> 标签,在主解析器尚未到达它们之前就发起网络请求。

与此同时,CSS —— 无论是来自 <style> 块还是链接的样式表 —— 会被解析为 CSSOM(CSS 对象模型,CSS Object Model),这是一棵包含已计算特异度(specificity)与层叠解析(cascade resolution)的样式规则树。CSSOM 的构建是**阻塞渲染(render-blocking)**的:因为原则上任何靠后的样式表规则都可能覆盖前面的规则,在获取完整的 CSSOM 之前,浏览器无法安全地开始绘制(这也是为什么经典的性能建议是将样式表保持小巧并尽早加载)。

渲染树、布局与绘制

在获得 DOM 和 CSSOM 后,Blink 将它们合并为渲染树(Render Tree):本质上就是 DOM,但过滤掉了所有实际不可见的节点(display: none 元素被完全排除,而 visibility: hidden 元素因仍占用空间而被保留),并标注了它们最终计算出的样式。

渲染树只知道要画什么,却不知道画在哪里。布局(Layout,有时也称为重排 reflow)会遍历该树,并计算每个节点相对于视口(viewport)的精确盒模型几何信息 —— 位置、宽度、高度、外边距。这本质上是一个全局操作:改变一个元素的宽度可能会产生连锁反应,引发每个兄弟节点与后代节点位置的变动,这也是为什么布局是管线中最昂贵的阶段之一,以及为什么在紧密循环中重复触发布局(布局抖动 / layout thrashing,例如交替读取 offsetHeight 并写入样式)是一个经典的性能反模式。

几何信息确定后,**绘制(Paint)**会把每个可见元素栅格化(rasterize)为实际像素 —— 填充文本、颜色、边框、阴影和图像 —— 并按层记录为有序的绘图指令列表(矩形、字形串 glyph runs、图像位块传送 image blits)。

合成:交由 GPU 处理

最终阶段合成(Compositing),正是实现无比丝滑的滚动与动画的关键所在。特定元素 —— 那些带有 CSS transform、opacity 过渡、will-change 提示、<video> 或 <canvas> 的元素 —— 会被提升到它们自己的**合成器层(compositor layer)中,本质上是一个独立的纹理(texture)。这些图层会被提交给 GPU 进程,GPU 进程在一个专门的合成器线程(compositor thread)**上将它们合成在一起,该线程与主 JavaScript/布局/绘制线程完全隔离。

这样做的好处在于:对 transform 和 opacity 应用动画可以完全跳过布局与绘制阶段,纯粹由 GPU 对预先栅格化的纹理进行重新定位来处理,这就是为什么即使主线程正忙于执行 JavaScript,这两个属性依然能稳定达到丝滑的 60/120fps。相比之下,对 width、top 或 left 应用动画,则会在每一帧中强行触发完整的“布局 → 绘制 → 合成”循环,因为几何形状本身发生了改变。

compositing_hints.css
1/* 低开销:提升至独立的 GPU 图层,在合成器线程上运行动画 —— 完全跳过布局与绘制。 */
2.modal-cheap {
3 transform: translateY(20px);
4 opacity: 0.9;
5 will-change: transform, opacity;
6}
7
8/* 高开销:在主线程上,每一帧动画都会强行触发完整的“布局 -> 绘制 -> 合成”Pass。 */
9.modal-expensive {
10 top: 20px;
11 left: 50%;
12 width: 320px;
13}

4. 推动 Web 前行的网络协议:从 HTTP/1.1 到 HTTP/3

如果字节传输耗时过长,渲染再快也毫无意义。浏览器底层的传输层所经历的激进重构,绝不亚于渲染引擎本身。

HTTP/1.1 与队头阻塞问题

HTTP/1.1 是一种明文请求-响应协议。严格来说,每个 TCP 连接一次只能有一个未完成的请求(管线化 pipelining —— 即无需等待响应即可发送多个请求 —— 在技术上虽然是被允许的,但由于中间网络设备对其实现过于糟糕且不一致,以至于浏览器从未默认开启它)。为了实现任何程度的并行性,浏览器被迫采用为每个源(origin)同时打开多个 TCP 连接的变通方案 —— 通常上限为 6 个左右。这种 Hack 技巧虽然有效,但伴随着沉重的代价:每个连接都需要建立自己的 TCP 握手,而在 HTTPS 下还需要建立 TLS 握手,并且每个连接都要从缓慢的初始拥塞窗口开始。

HTTP/2:单条连接上的多路复用

HTTP/2 用二进制分帧层(binary framing layer)取代了 HTTP/1.1 的明文分帧,将请求和响应拆分为标记有流 ID(stream ID)的小型帧,全部在单条 TCP 连接上交错传输。这实现了真正的多路复用(multiplexing):数十个请求和响应可以在一条连接上同时处于传输状态(in flight),消除了对 6 个连接变通方案的需求以及随之而来的冗余握手开销。HTTP/2 还引入了 HPACK 标头压缩(因为发往同一源的请求中 HTTP 标头存在高度重复),以及更具争议性的服务器推送(server push)(由于实践证明其难以调优且效果往往不如 <link rel="preload"> 等更简单的技术,到 2022 年时已在实践中被广泛弃用)。

HTTP/2 解决了应用层的队头阻塞 —— 但它从其传输层继承了一个更深层的问题。TCP 保证严格按序、按序列递送字节。如果在那条唯一的共享 TCP 连接中的任何地方丢失了一个数据包,所有多路复用的 HTTP/2 流都会停滞,因为在丢包被重传且字节流重新恢复有序之前,TCP 不会将内核的接收缓冲区向上交付给应用 —— 即使丢失的数据包可能仅属于几十个活跃流中的某一个。这就是传输层队头阻塞(transport-layer head-of-line blocking),只要底层传输仍采用 TCP,应用层再怎么聪明也无法修复它。

HTTP/3:放弃 TCP,拥抱 QUIC

HTTP/3 的决定性决策是激进的:它完全放弃了 TCP,转而运行在 QUIC 之上 —— 一种建立在 UDP 之上的全新传输协议。这听起来像是退步 —— UDP 完全不提供任何排序或可靠性保证 —— 但这恰恰是关键所在。QUIC 在传输层自行重新实现了可靠性与拥塞控制,但至关重要的一点在于,它是**按流(per-stream)**而非针对整个连接来执行此操作的。如果携带流 4 数据的数据包丢失,只有流 4 会停滞等待重传;流 1、2 和 3 则继续不受干扰地向应用程序交付数据。队头阻塞现在被局限在真正丢失数据的单个流中,而非影响整条连接。

QUIC 还将传输握手与加密握手折叠融合在一起。过去 TCP + TLS 1.3 需要先进行 TCP 握手然后再进行独立的 TLS 握手(增加了往返时间 RTT),而 QUIC 将 TLS 1.3 直接整合到自身的握手中。对于客户端最近访问过的服务器的重复连接,它支持 0-RTT 恢复(0-RTT resumption):客户端可以使用从上一会话缓存的加密参数,在第一批数据包中直接发送加密的应用数据 —— 在有用的数据开始流动之前,完全无需等待服务器的往返。(0-RTT 数据存在理论上的重放攻击风险,这也是为什么服务器会限制允许使用它的请求类型,通常仅限幂等请求。)

QUIC 另一个常被低估的特性是连接迁移(connection migration):由于 QUIC 连接是由连接 ID(Connection ID)而非传统的 TCP 四元组(源 IP、源端口、目的 IP、目的端口)来标识的,因此客户端在切换网络时 —— 例如走出门外时从 WiFi 切换到蜂窝移动数据 —— 无需中断并重新建立连接。

属性HTTP/1.1HTTP/2HTTP/3
传输协议TCPTCP基于 UDP 的 QUIC
多路复用无(需要多条连接)是,单条连接是,按流独立
队头阻塞严重(按连接)仅传输层(单包丢失阻塞所有流)彻底解决(按流隔离)
标头压缩无HPACKQPACK
握手方式TCP + 独立 TLS 握手TCP + 独立 TLS 握手传输与加密合并握手
0-RTT 恢复否否是
连接迁移否(绑定 IP/端口)否(绑定 IP/端口)是(基于 Connection ID)

5. 结语:WebAssembly 与填补原生性能差距

迄今为止涵盖的每一层 —— 沙盒化的多进程架构、V8 的推测性 JIT、GPU 加速的渲染管线以及 QUIC 的低延迟传输 —— 都是为了服务于同一个目标:让浏览器充当一等应用平台(first-class application platform)。最后一个主要差距在于最重型工作负载的原始计算吞吐量:视频/照片编辑套件、CAD 工具、完整 3D 游戏引擎以及科学计算,在这些领域,即便是 V8 优化后的机器码,也承受着来自 JavaScript 动态类型检查与垃圾回收停顿的残留开销。

WebAssembly (Wasm) 填补了这一差距。它是一种紧凑的二进制指令格式,被设计为编译目标(target)而非手写的语言 —— C、C++、Rust 和 Go 代码可以直接编译为运行在同一个沙盒化渲染进程内的 Wasm 模块,其速度接近原生机器码。因为 Wasm 是静态类型的且经过提前验证(ahead of time),从而避开了 JavaScript 引擎不得不进行推测的动态类型开销。Wasm 模块与 JavaScript 共享线性内存缓冲区,并通过轻量的 JS 胶水代码进行调用,使得现有的原生代码库 —— 物理引擎、视频编解码器、CAD 内核 —— 几乎无需修改即可直接移植到浏览器中。

这正是 Figma 如何在标签页中运行 C++ 渲染引擎、AutoCAD 和 Photoshop 如何提供具备真实编辑性能的浏览器版本,以及 Unreal Engine 和 Unity 如何将 Web 作为可玩游戏 Demo 构建平台的原因。像 WebAssembly 系统接口(WASI) 这样新兴的努力,如今正将 Wasm 的沙盒化、可移植执行模型彻底推向浏览器之外,进入边缘计算与服务端运行时 —— 将一项旨在解决浏览器性能问题的技术,转变为用于广义计算的通用、安全执行格式。

在 Netscape 发布文档查看器的三十年后,“浏览器”可以说是大多数人运行的最复杂的消费级软件:从最纯粹的工程学意义上讲,它就是一个操作系统,只不过恰好在屏幕上的一个矩形区域内渲染自己罢了。


参考文献与延伸阅读


技术术语表

术语定义
BlinkGoogle 的 HTML/CSS 渲染引擎,于 2013 年从 WebKit 分叉而来,如今被 Chrome、Edge、Opera 以及绝大多数其他基于 Chromium 的浏览器共享。
站点隔离(Site Isolation)Chromium 的一种安全架构,为每个独立站点(包括跨源 iframe)分配专用的渲染进程,以抵御 Spectre 级别的内存读取攻击。
IgnitionV8 的字节码解释器,负责快速启动执行并收集类型反馈分析数据。
TurboFanV8 的优化 JIT 编译器,为“热点”函数生成推测性类型专门化的原生机器码。
隐藏类(Hidden Class / Map)V8 内部对 JavaScript 对象属性“形状”的表示,实现类似于静态语言的基于偏移量的属性访问。
内联缓存(Inline Cache / IC)每个调用点专属的缓存,记录最近观察到的隐藏类,允许 V8 在重复执行时跳过完整的属性查找。
CSSOMCSS 对象模型(CSS Object Model)—— 已解析样式表规则的树状表示,与 DOM 结合以生成渲染树。
合成器线程(Compositor Thread)独立于主 JS/布局线程的专用线程,处理 GPU 图层合成,实现独立于主线程工作的丝滑动画。
队头阻塞(Head-of-Line Blocking)一种停滞状态,在同一个通道上多路复用传输时,单个数据单元的丢失或延迟会阻塞无关数据的交付。
QUIC基于 UDP 的传输协议(RFC 9000),提供按流独立的可靠性、集成 TLS 1.3 握手、0-RTT 恢复以及连接迁移。
0-RTT 恢复(0-RTT Resumption)QUIC/TLS 1.3 的一项特性,允许重返的客户端在第一批数据包中直接发送加密的应用数据,无需等待服务器往返。
WebAssembly (Wasm)一种静态类型的沙盒化二进制指令格式,用作编译目标,在浏览器运行时内部实现接近原生的性能。
返回博客
分享:

保持关注

第一时间获取最新文章、思考及动态。