这是专门探索 JavaScript 及其构建组件系列的第 6 期。在识别和描述核心元素的过程当中,咱们还分享了构建 SessionStack 时使用的一些经验法则 —— 这是一个轻量级的 JavaScript 应用程序,但必须强大且性能卓越,才能帮助用户实时查看和重现其 Web 应用的缺陷。javascript
此次咱们将剖析 WebAssembly 的工做原理,更重要的是在性能方面分析它与 JavaScript 的差别:加载时间、执行速度、垃圾回收、内存使用状况、平台 API 调用、调试、多线程和可移植性。html
咱们构建 Web 应用程序的方式正处于革命的边缘 —— 仍然是初级阶段,但咱们对 Web 应用程序的见解正在发生变化。前端
WebAssembly(也叫做 wasm)是一种高效且底层的给 web 使用的字节码。html5
WASM 让你可以用 JavaScript 以外的语言(例如 C、C++、Rust 或其余)编写程序,而后将其(提早)编译到 WebAssembly。java
其结果是 Web 应用程序加载和执行速度都很是快。android
为了加载 JavaScript,浏览器必须加载全部文本形式的 .js
文件。ios
WebAssembly 在浏览器中加载速度更快,由于只需经过互联网传输已编译的 wasm 文件。而 wasm 是一种很是简洁的二进制格式的底层类汇编语言。git
今天 Wasm 的运行速度只比本地代码(native code)执行慢 20%。不管如何,这是一个惊人的结果。这是一种编译到沙盒环境中的格式,而且在不少约束条件下运行,以确保它没有或者很难有安全漏洞。与真正的本地代码相比,速度损失很小。更重要的是,它将在将来更快。github
更好的是,它与浏览器无关 —— 目前全部主要引擎都增长了对 WebAssembly 的支持,而且执行时间相近。web
为了理解 WebAssembly 与 JavaScript 相比执行得有多快,你应该首先阅读咱们关于 JavaScript 引擎的文章。
咱们来看看大概看看 V8 中会发生什么:
V8 的方法:延迟编译
在左边,咱们有一些 JavaScript 源代码,包含 JavaScript 函数。首先须要解析它,以便将全部字符串转换为词法标记(token)并生成抽象语法树(AST)。AST 是 JavaScript 程序逻辑的内存表示。一旦生成了这种表示,V8 会直接跳到机器码。过程基本上是遍历语法树,生成机器代码,最后获得编译好的函数。没有真正的尝试来加速它。
如今,咱们来看看 V8 流水线在下一阶段的功能:
V8 流水线设计。
此次咱们有了 TurboFan —— V8 的优化编译器之一。随着你的 JavaScript 应用的运行,大量代码运行在 V8 中。TurboFan 能够监控某些代码是否运行缓慢,是否存在瓶颈和热点来优化它们。它把这些代码推到编译器后端 —— 一个优化的 JIT,这个后端可为那些消耗大部分 CPU 的函数建立更快的代码。
它解决了上面的问题,但这里的问题在于,分析并决定优化哪些代码的过程也会消耗 CPU。这反过来又意味着更高的电池消耗,特别是在移动设备上。
好了,wasm 并不须要全部的这些 —— 它会被插入工做流中,以下所示:
V8 流水线设计 + WASM。
Wasm 在编译阶段就已经优化好。最重要的是,也再也不须要解析过程。你有了一个已优化的二进制文件,它能够直接挂接到生成机器码的编译器后端。全部优化都在编译器前端完成。
这让执行 wasm 更有效率,由于流程中的不少步骤均可以简单地跳过。
WebAssembly 可信和不可信状态。
举个例子,C++ 程序中的内存是一个连续的区块,其中并无「空隙」。有助于提升安全性的 wasm 的特性之一是,执行栈与线性内存分离的概念。在 C++ 程序中,你有一个堆,你从底部分配堆内存,并从堆顶部获取栈空间。这就有可能造出一个指向栈空间的指针来玩弄那些本不该该接触到的变量。
这是不少恶意软件所利用的缺陷。
WebAssembly 采用彻底不一样的模型。执行栈与 WebAssembly 程序自己是分开的,所以你没法修改栈变量等内容。并且,函数中使用整数偏移而不是指针。函数指向一个间接函数表。而后经过这些计算出的直接数字跳转到模块内部的函数中。这种设计方式使得你能够加载多个 wasm 模块,并排排列,平移全部的索引,互不影响。
有关 JavaScript 中内存模型和管理的更多信息,能够查看咱们很是详细的关于此主题的文章。
你已经知道 JavaScript 的内存管理是使用垃圾收集器处理的。
WebAssembly 的状况有点不一样。它支持手动管理内存的语言。你的 wasm 模块能够自带 GC,但这是一项复杂的任务。
目前,WebAssembly 是围绕 C++ 和 RUST 用例设计的。因为 wasm 是很是底层的,所以只有汇编语言上一层的编程语言才易于编译。C 可使用普通的 malloc,C++ 可使用智能指针,Rust 使用彻底不一样的形式(彻底不一样的主题)。这些语言不使用 GC,所以它们不须要那些复杂的运行时事务来跟踪内存。WebAssembly 对他们来讲是天做之合。
另外,这些语言并非 100% 被设计用于调用复杂的 JavaScript 事物,如操做 DOM。彻底在 C++ 中编写 HTML 应用是没有意义的,由于 C++ 不是为它设计的。在大多数状况下,当工程师编写 C++ 或 Rust 时,他们的目标是 WebGL 或高度优化的库(例如繁重的数学计算)。
可是,未来 WebAssembly 也将支持不附带 GC(但须要垃圾回收)的语言。
取决于执行 JavaScript 的运行时,不一样特定于平台的 API 能够经过 JavaScript 应用程序直接访问。例如,若是在浏览器中运行 JavaScript,你能够经过一系列 Web APIs 来控制 web 浏览器 / 设备的功能,而且可使用例如 DOM、CSSOM、WebGL、IndexedDB、Web Audio API 等等
好吧,WebAssembly 模块没法直接调用任何平台 API。一切都是由 JavaScript 代理的。若是你想在 WebAssembly 模块中调用的某些平台特定的 API,则必须经过 JavaScript 调用它。
例如,若是你想用 console.log
,必须经过 JavaScript 调用它,而不是你的 C++ 代码。这些 JavaScript 调用的成本会比较高。
也并不老是如此。规范将在将来为平台 API 提供 wasm 接口,而且你将可以在没有 JavaScript 的状况下发布应用程序。
当你压缩 JavaScript 代码时,须要一种正确调试它的方法。这就是源码映射大显身手地方。
基本上,源码映射是一种将整合/压缩文件映射回构建前状态的方法。当你构建线上版时,压缩和组合 JavaScript 文件时将生成一个包含原始文件信息的源码映射。当你在生成的 JavaScript 中查询某一行号和列号时,能够在源码映射中查找代码的原始位置。
WebAssembly 目前不支持源码映射,由于暂时没有规范,但最终会有的(可能很快)。
当在 C++ 代码中设置断点时,你将看到 C++ 代码而不是 WebAssembly。至少这是目标。
JavaScript 在单线程上运行。有不少方法能够发挥事件循环和异步编程优点,详见咱们关于该主题的文章。
JavaScript 也使用 Web Workers,但他们有一个很是具体的用例 —— 基本上,阻止主 UI 线程的任何重 CPU 计算均可以从 Web Worker 中受益。可是 Web Workers 没法访问 DOM。
WebAssembly 目前不支持多线程。可是将来可能会。Wasm 将会和本地线程更近(例如 C++ 型线程)。拥有「真实」的线程将在浏览器中创造出许多新的机会。固然,这也将打开更多滥用可能性的大门。
现在,JavaScript 几乎能够在任何地方运行,从浏览器到服务器端甚至嵌入式系统。
WebAssembly 设计目标是安全且可移植。就像 JavaScript 同样。它将运行在支持 wasm 的每一个环境中(例如每一个浏览器)。
WebAssembly 具备与 Java Applets 初期尝试实现的移植性相同的可移植性目标。
在 WebAssembly 的第一个版本中,主要关注 CPU 占用大的计算(例如处理数学)。想到的最主流的用途是游戏 —— 那里有大量的像素操做。你可使用你习惯的 OpenGL 绑定在 C++ / Rust 中编写应用,并将其编译为 wasm。它会在浏览器中运行。
看看这个(在 Firefox 中运行)—— s3.amazonaws.com/mozilla-gam…。它使用虚幻引擎。
另外一种使用 WebAssembly 可能有意义(性能方面)的场景是实现一些这是一个 CPU 密集型的库。例如,一些图像处理库。
如前所述,因为大多数处理步骤都是在编译期间提早完成的,所以 wasm 能够减小移动设备上的电池消耗(取决于引擎)。
未来,即便你实际上没有编写代码,你也可使用 WASM 二进制文件。能够在 NPM 中找到开始使用此方法的项目。
对于 DOM 操做和大量的平台 API 操做,固然用 JavaScript 更好,由于它不会增长额外的开销,而且具备原生的 API。
在 SessionStack,为了编写高度优化且高效的代码,咱们不断突破 JavaScript 性能的机极限。咱们的解决方案须要提供超快的性能,由于咱们不能阻碍客户应用自己。将 SessionStack 集成到线上 Web 应用或网站后,它会开始记录全部内容:全部 DOM 更改、用户交互、JavaScript 异常、堆栈跟踪、失败的网络请求和调试数据。全部这些都在你的线上环境中进行,但不会影响产品的任何体验和性能。咱们须要大量优化咱们的代码并尽量使其异步。
并且不仅是库!当你在 SessionStack 中重放用户会话时,咱们必须渲染在发生问题时用户浏览器中发生的全部事件,而且必须重构整个状态,容许你在会话时间线中来回跳转。为了作到这一点,咱们正在大量使用 JavaScript 提供的异步能力,由于缺乏更好的选择。
借助 WebAssembly,咱们可以将一些最繁重的处理和渲染交给更适合作这个工做的语言,同时将数据收集和 DOM 操做留给 JavaScript。
若是你想试试 SessionStack,能够从这里免费开始。免费版能够提供 1000 会话 / 月。
资源:
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。