<audio id="audio" title="02 | 历史篇：为什么会有 WebAssembly 这样一门技术？" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/2c/b3/2cccd686797c69c29ed4756b4dec83b3.mp3"></audio>

你好，我是于航。

在开始真正学习 Wasm 这门技术之前，我想先来问你一个问题：你有没有思考过，在 Web 技术的历史发展长河中，为什么会出现 Wasm 这样一门技术？现有的这些 Web 技术，又存在着哪些无法解决的问题？

要知道，所有新兴技术的诞生都一定有它存在的意义，或者要去解决的问题。比如  jQuery 之于浏览器的兼容性、Vue.js / React.js 之于 Web 应用的构建模式。

虽然用前端框架和库来类比 Wasm 不算十分合适，但我想阐述的是，Wasm 的出现也并非偶然。在这节课的内容中，我们就来一起看看 Wasm 诞生背后的那些故事。相信在学习完本课程后，你会对 Wasm 有了一些新的了解。而这些了解有时可能比一项技术本身更加重要。

## JavaScript 的发展和困境

1995 年末，Brendan Eich 仅用了 10 天时间便发明出了 JavaScript 编程语言，而在随后的二十多年中，JavaScript 已经成为了不可动摇的，用于开发 Web 前端应用的必备编程语言之一。

不仅如此，随着后来诸如 React Native、Electron 以及 Vue.js 等各类框架的不断涌现，JavaScript 曾经一度成为 GitHub 语言排行榜的年度冠军。JavaScript 也因此被广泛应用到了各行各业、各个领域的各类项目中。

虽说  JavaScript  的应用场景如此广泛，但也会有它自己的烦恼。下面我们就从  Web  应用层面以及 JavaScript  语言本身，来看看它究竟在愁些什么。

### Web 应用规模的急速增长

随着移动互联网的发展和各种形式经济活动的不断展开，运行在浏览器中的各类 Web 应用，它们的体积与复杂性随着时间的推移在不断发展。为了能够在浏览器中高效运行这些不断“变大”的 Web 应用，浏览器厂商们也在不断地寻求着各种“黑科技”来优化浏览器的性能。

但与日益庞大和复杂化的 Web 应用相比，浏览器对自身性能的优化可谓是举步维艰。不难预见，当“复杂化”与“性能优化”的速度之比不断变大时，迟早有一天，浏览器会再也无法支撑起这些庞大 Web 应用的运行。

据相关数据统计，截止 2019 年底，全世界一共有约 16 亿个可索引网页，而其中的 95% 都在使用 JavaScript。在这些网页中，大约有 45% 的网页创建于最近 5 年。而 2015 年，ECMAScript 2015 (ES6) 诞生，也标志着 JavaScript 开始进入了标准一年一更新的节奏中。

现代的大多数网页，都会使用较新的 JavaScript 语法标准进行开发，然后在发布时使用诸如 Babel 等工具，将这些新的 JavaScript 语法转换为对应的 ES5 旧版本语法，来兼容旧版本浏览器。但这样做，产生的各类 Polyfill 代码，会极大地增加整个 Web 应用的体积。

同时，在 Web 应用的实际运行过程中，大量的 JavaScript 代码也会降低应用的整体运行效率。Twitter 曾尝试直接以 ES6+ 版本代码的形式，来发布整个 Web 应用。通过这种方式所减少的 Polyfill Bundle 文件的大小，竟然可以达到应用所使用的全部 JavaScript 代码的 83%。

### JavaScript 的弱类型之殇

除了上面我们讲到的，浏览器性能优化与 Web 应用规模日益增大，这两者行进速度的“不协调”所可能带来的问题之外，JavaScript 语言本身也有着其自身的“弱点”。而由于这些“弱点”所带来的妥协，使得浏览器在面对庞大的 Web 应用时，也会显得力不从心。

可以说，JavaScript 是一个“动态类型”的编程语言。在实际编码过程中，我们不需要为每一个变量指定对应类型。变量具体类型的推导过程，会被推迟到代码的实际运行时再进行。JavaScript 这种动态类型语言所独有的特性，在某种程度上相较于静态类型语言而言，会带来额外的运行时性能开销。

下面我们来一起想象一下， JavaScript 引擎在执行表达式 “x + y” 时的具体流程。这里 x 与 y 分别是在一段 JavaScript 代码中定义的两个变量，当引擎执行到 “x + y” 时，对于运算符 “+” 来说，位于其左右两侧的操作数可以是 JavaScript 中任何有效类型的组合，比如 “{} + []”、“[] + null”、“1 + 2” 等等。因此，引擎在对 “+” 运算符表达式进行求值时，会根据 ECMAScript 标准中规定的 “+” 运算符的语义，来对表达式进行求值。

通过下图你可以看到， 在 ECMAScript 标准中定义的，“+” 运算符的运行时求值流程，实际上十分复杂和繁琐。这也是相对于静态语言来说，JavaScript 很少能够进行优化的地方。

在现代的 JavaScript 引擎中，尽管可以使用诸如 JIT 等技术来提高代码的执行效率，但在实际使用中，如果代码执行没有遵守 JIT 优化路径中特定 Guard 的要求，“去优化”的过程，也同样会影响引擎的整体执行效率。而这些影响都是由于 JavaScript 的“动态性”导致的。

<img src="https://static001.geekbang.org/resource/image/e4/47/e40df57ac5cc30613170717291fd8b47.png" alt="" title="图片来自 ECMAScript@2020 官方标准文档
">

## 最初的尝试 —— NaCl 与 PNaCl

JavaScript 的发展困境在逐渐显现，人们对 Web 性能的担忧也在与日俱增，人们永远没有停下优化的脚步。NaCl 是由 Google 在 2011 年于 Chrome 浏览器中发布的一项技术，该技术旨在提供一个沙盒环境，可以让基于 C/C++ 语言编写的 Native 应用，安全地运行在浏览器中。NaCl 的全称 “Native Client” 也暗示了这一点。

如下图所示，一个标准 NaCl 应用的组成结构，与普通的 JavaScript Web 应用十分类似。NaCl 模块作为应用的一部分，主要用来进行复杂的数据处理和运算，JavaScript 则负责处理应用与外部用户的交互逻辑。NaCl 实例与 JavaScript 代码之间可以通过“订阅/发布”模型，来互相传递消息。

<img src="https://static001.geekbang.org/resource/image/b9/cb/b95925e8a831ec3266727fedfc2175cb.png" alt="" title="图片来自 Chrome 官方相关文档
">

理想虽好，但现实却存在着很多问题。通常，一个 NaCl 模块文件需要在开发者本地进行编译，然后才能够在浏览器中使用。而本地编译的模块文件通常仅含有架构相关（architecture-dependent）的代码，因此没有办法直接在其他类型的系统中使用。

一个完整的 NaCl 应用，在分发时需要提供支持多个架构平台（X86_32 / X86_64 / ARM 等）的模块文件。浏览器在实际使用时，会根据当前系统的具体架构类型，来动态地选择，对应合适的模块文件进行使用。

不仅如此，由于 NaCl 模块“平台依赖”的特殊性，因此 NaCl 模块进行分发的过程，仅能够在 Chrome Web Store 中进行。 另一方面，如果你想要将已经存在的 C/C++ 代码库编译至 NaCl，并在浏览器中使用，你还需要通过名为 Pepper 的库来对这些代码进行重写。

Pepper 提供了很多包装类型，以及用于和浏览器进行交互的 API，比如 “PP_Bool” 等。这些 API 和特殊类型可以便于整合传统 C/C++ 代码与 Web 浏览器的沙盒环境。

鉴于 NaCl 存在的“平台依赖”问题，Google 在后期又推出了名为 PNaCl 的技术。这里名字中多出来的 “P” 代表着 “Portable”，也就是“可移植”的意思。

PNaCl 采用了不一样的生命周期，参考下图我们可以看到，相较于 NaCl 模块直接包含有平台架构相关的代码，PNaCl 将源 C/C++ 代码编译到一种中间代码。这些中间代码会在浏览器实际加载这个 PNaCl 模块时，再被转换为对应的平台相关代码。因此，对于 PNaCl 模块而言，分发的过程变得更加简单，且不用担心移植性的问题。

<img src="https://static001.geekbang.org/resource/image/09/46/09ef8002f3411732fc1ac513814ab646.png" alt="" title="图片来自 Chrome 官方相关文档">

不过，即使是对于 PNaCl 这类“可移植性”已经不再成为问题的技术而言，它们的面前还有很多“大山”难以逾越。比如：“需要使用 Pepper 重写 C/C++ 代码，标准较为封闭、仅 Chrome 浏览器支持”等等。

总而言之，无论是 NaCl 还是 PNaCl，它们都已经成为过去。现在，如果你再次回到 NaCl / PNaCl 在 Google 的官方文档网站，你会发现如下这样一段声明。Wasm 将会作为新一代的技术，接替并继续传承 Google 赋予给 NaCl / PNaCl 的使命。

<img src="https://static001.geekbang.org/resource/image/e0/0c/e0c11b3ec2da8890a096509992cc1f0c.png" alt="" title="图片来自 Chrome 官方相关文档">

## Wasm 的前身 —— ASM.js

除了 NaCl 与 PNaCl，另一个不可不提的技术便是 Mozilla 于 2013 提出的 ASM.js。同前两者一样，ASM.js 的设计目标也是为了能够在 JavaScript 语言之外，为“构建更高性能的 Web 应用”这个目标，提供另外一种实现的可能。

“ASM.js 是 JavaScript 的一个严格子集。它是一种可用于编译器的目标语言，低层次且高效。该目标语言有效地为内存不安全语言（如 C/C++），描述了一个沙盒虚拟机运行环境。静态和动态验证相结合的方式，使得 JavaScript 引擎能够使用 AOT 等优化编译策略来验证 ASM.js 代码”。这是 Mozilla 官方给出的关于 “ASM.js 是什么？” 这个问题的解答。

乍一看这段解释，可能会有点抽象和复杂。但实际上，我们只需要知道两件事情。

第一，ASM.js 是 JavaScript 的严格子集。这也就意味着，对于一段 ASM.js 代码，JavaScript 引擎可以将它视作普通的 JavaScript 代码来执行，这便保障了 ASM.js 在旧版本浏览器上的可移植性。

第二，ASM.js 使用了 “Annotation（注解）” 的方式来标记代码中包括：函数参数、局部/全局变量，以及函数返回值在内的各类值的实际类型。

当 JavaScript 引擎满足一定条件后，便会通过 AOT 静态编译的方式，将这些被 Annotation 标记的 ASM.js 代码，编译成对应的机器码并加以保存。当 JavaScript 引擎再次执行（甚至在第一次执行）这段 ASM.js 代码时，便会直接使用先前已经存储好的机器码版本。因此，引擎的性能会得到大幅的提升。

对于一段标准 ASM.js 代码的具体组成形式，你可以参考下面给出的这段代码，以便有一个更加直观的印象。

```
function asm (stdin, foreign, heap) {
  &quot;use asm&quot;;
  
  function add (x, y) {
    x = x|0;  // 变量 x 存储了 int 类型值；
    y = y|0;  // 变量 y 存储了 int 类型值；
    var addend = 1.0, sum = 0.0;  // 变量 addend 和 sum 默认存放了&quot;双精度浮点&quot;类型值；
    sum = sum + x + y;
    return +sum;  // 函数返回值为&quot;双精度浮点&quot;类型；
  }
  return { add: add };
}


```

在这段 JavaScript 代码中，最为重要的是函数 “asm” 在其函数体定义开头处使用的 “use asm” 指令。这个指令将会在代码执行过程中“告诉” JavaScript 引擎，当前这个函数体内的代码可以按照 ASM.js 代码，来进行相应的优化和处理。

实际上，上述这样的一个 JavaScript 函数，便定义了一个标准的 ASM.js 模块。模块内部可以通过 return 的方式，导出包含有若干内联方法的对象。这些方法可以在外部的 JavaScript 代码中进行调用。

在上述 asm 模块内定义的内联函数 add 中，我们在其开头的前两行代码通过 “x|0” 和 “y|0” 的方式，分别对变量 x 与 y 的值类型进行了标记。而这种方式便是我们之前提到的 ASM.js 所使用的 Annotation。

当 JavaScript 引擎在编译这段 ASM.js 代码时，便会将这里的变量 x 与 y 的类型视为 int 整型。同样的，还有我们对函数返回值的处理 “+sum”。通过这样的 Annotation，引擎会将变量 sum 的值视为双精度浮点类型。类似的，ASM.js 在标准中还规定了其他的诸多 Annotation 形式，可以将变量值标记为不同的类型，甚至对值类型进行转换。

为了确保上述的这样一个 JavaScript 函数，能够被当做一个标准的 ASM.js 模块进行必要的优化处理，JavaScript 引擎通常会在实际编译加载这些模块前，进行很多必要的检查验证工作。

因此，并不是说只要为函数添加了 “use asm” 指令，并且为使用到的变量添加 Annotation 之后，JavaScript 引擎就会通过 AOT 的方式来优化代码的执行。所以这也是为什么我们先前提到的，ASM.js 通常被作为一种可用于编译器的，低层次且高效的目标语言，而不是用于手写。

## 从过去到未来

时间来到2015年5月。Chrome 团队的 Ben 正在为 V8 设计一种新的 Prototype（原型），而另一位团队成员 Rosbery ，正在为这种 Prototype 设计对应的字节码格式。实际上，这个 Prototype 和对应的字节码格式，便是如今 Wasm 所分别对应的 WAT 可读文本格式与二进制字节码格式。在当时的谷歌内部，这两部分暂时被称为 ml-proto 与 v8-native-prototype。

随着 V8 团队对 ml-proto 与 v8-native-prototype 的不断修改和优化，它们最终便成为了 Wasm 早期标准的一部分。与此同期出现的，还有一个名为 “sexpr-wasm” 的内部工具 ，在当时这个工具用于对这两种格式进行相互转换。随着 Wasm 的标准化，它也同样成为了 Wasm 常用调试工具的一部分，这也就是我们所熟知的 —— WABT。

Chrome V8 团队作为参与过 PNaCL 与 ASM.js 这两个标准制定的团队，在设计和实现 Wasm 时也同样参考了很多从这两种技术中总结下来的优缺点。而这些经验也将会帮助 Wasm 做好准备，避开那些曾经走过的坑。最后，这些经验使得 Wasm 能够以一种更好的方式，展现在人们的面前。

## 总结

好了，讲到这，今天的内容也就基本结束了。最后我来给你总结一下。

实际上在 Wasm 真正出现之前，人们就已经开始尝试探索各类新型技术以赋予 Web 应用更高的运行效率。

从 NaCl、PNaCl 到 ASM.js，它们主要有三点共同特征：<br>
1.源码中都使用了类型明确的变量；<br>
2.应用都拥有独立的运行时环境，并且与原有的 JavaScript 运行时环境分离；<br>
3.支持将原有的 C/C++ 应用通过某种方式转换到基于这些技术的实现，并可以直接运行在 Web 浏览器中。

Wasm 这项技术的设计与实现，离不开从这些“前辈”们身上学习到的经验。从表面上来看，互联网技术迭代飞快。但实际上，当稍微深入和总结之后，你就会发现其实它们都有着基本相同的，想要去解决的目标问题，比如对于性能的执著要求。以及十分类似的技术解决方案，比如尽最大可能去确定那些能够确定、不会发生变化的部分（比如类型），然后再以此为基础进行优化。 Wasm 也不例外。

## 课后思考

最后，我们来做一个思考题吧。

你觉得就目前的 Web 技术领域而言，存在着哪些困境？或者说需要去解决和优化的地方？

今天的课程就结束了，希望可以帮助到你，也希望你在下方的留言区和我参与讨论，同时欢迎你把这节课分享给你的朋友或者同事，一起交流一下。
