Day23 X WebAssembly

也许你早就听过 WebAssembly 这个词,传说中它可以让 C, C++, Rust 等系统语言的程序码在浏览器上执行,解决 JS 的效能已经快要逼近极限的问题,并可以做一些以前对於浏览器来说可能相对吃效能的任务。

有些传言甚至说 WebAssembly 未来会取代 JavaScript,然而事情真的是这样吗?JS 开发者要失业了吗?我需要赶快去学 Rust 或 C++ 以保住饭碗吗? WebAssembly 又到底是什麽?是怎麽运作的?今天我们就来了解一下 WebAssembly 的基础观念吧!

(本篇只会介绍 WebAssembly 最基本的概念与它对於前端开发者而言带来的可能性,不会有太深入的介绍,如果原本抱有许多期待的读者只能说声抱歉了,铁人赛果然不简单,写太多我会断赛的 ? 不过我有预计未来了解深入一点後在 Medium 再撰写更深入探讨的文章,如果不想错过的读者可以追踪我的 medium 喔!)

直译式语言 vs 编译式语言

JavaScript 是一门直译语言,在执行时会一行一行动态地将程序码直译为机器码并且执行,这种语言通常会是动态语言,并且在型别与程序上会较有弹性,身为前端开发者,应该对这些特性都不陌生了。而像是 C, C++ 则是属於编译语言,编译语言在执行前会先透过编译器(compiler)将程序码编译成电脑看的懂的机器码(Machine Code),最後再执行,这种语言通常会是静态的语言且具有型别检查的机制与高性能。

所以不论 JavaScript 经过优化後变得再怎麽快,都会受限於直译语言的特性,没办法达到像是编译语言那样的性能。

既然编译语言那麽快,为什麽不直接在浏览器上跑 C++ 之类的语言呢?主要原因有两个:

  • 浏览器需要透过网路抓取编译後的档案,因为编译後的档案通常都很大,因此传输会需要花费不少时间。
  • 如果是先传过来再编译,等程序编译完也会需要一定的时间。

Let's Welcome WebAssembly !

既然知道 JavaScript 因为是直译语言的关系,就算要优化也会达到一个天花板并遇到瓶颈,但是要直接在浏览器跑编译语言又会遇到限制,那该如何是好呢?Let's Welcome WebAssembly!

首先来看看 MDN 官方文件是怎麽介绍它的:

WebAssembly 是一种新的低阶程序语言,可在今日的网页浏览器中被执行 —— 它是低阶的类组合语言,具有严谨的二进位格式,能以接近原生应用程序的效能执行,并提供如 C/C++/Rust 等语言一个构建目标,使它们能在 Web 上被执行。他也被设计为可与 JavaScript 共存,允许两者一同工作。

WebAssembly 已经成为了 W3C 的标准,在各家浏览器厂商间建立了一制性的规范,简单来说它可以让编译语言写的程序码透过编译器编译成二进位制的 wasm 档,再放入到 JS Engine 准备好的 wasm compiler 当中解码并编译为机器码,从而得以被浏览器执行。

曾经看到很多人说「WebAssembly 未来会取代 JavaScript」,但是事实是

WebAssembly 的目的不是取代 JavaScript!
WebAssembly 的目的不是取代 JavaScript!
WebAssembly 的目的不是取代 JavaScript!

而更像是与 JavaScript 一起合作的角色,补足一些在 JS 里不易达成的耗性能操作。

WebAssembly 的适用场景

目前主要应用在对性能要求较高的应用中,例如:

  • AR
  • VR
  • 游戏开发
  • 数学计算
  • IOT
  • 区块链
  • Edge Computing
  • 图片与影像的编辑操作

如果想知道有哪些应用实际有使用 WebAssembly 的读者十分建议阅读这篇文章,另外也十分推荐前阵子一位大大在前端社群分享利用 WebAssembly module 实作 QRcode 扫描的心得

Web 开发者的好选择 - AssemblyScript

我们首先来看看 .wasm 档案的内容格式是长什麽样子

(module
  (import "console" "log" (func $log (param i32 i32)))
  (import "js" "mem" (memory 1))
  (data (i32.const 0) "Hi")
  (func (export "writeHi")
    i32.const 0  ;; pass offset 0 to log
    i32.const 2  ;; pass length 2 to log
    call $log))

虽然说我们可以直接撰写这样的语法,但毕竟它的可读性并不高,也比较难上手,所以一般在开发 WebAssembly 时还是比较常透过 C, C++, Rust 等程序语言来开发,再透过 Emscripten 编译成 WebAssembly。

但是对前端开发者来说,这意味着我们需要再额外去学习一种新的程序语言(学习新的语言也未尝不好,笔者就有在额外花时间学习 Rust,虽然一开始会觉得跟 JS 的世界很不一样有点不舒服,但的确开拓了一些视野与未来的可能性。)

其实对於前端开发者来说,还有一个更适合我们的选择 - AssemblyScript

简单来说它可以让我们以 TypeScript 的语法来开发 WebAssembly 程序,不过两者间还是有一些不同,例如在 AssemblyScript 中数字的型别是使用如 i32, u32 等特定整数或浮点数的型别,而不是在 TypeScript 中使用的 number 型别。因为 AssemblyScript 只允许 TypeScript 的有限功能子集,因此如果熟悉 TypeScript 的开发者应该可以迅速上手 AssemblyScript。

AssemblyScript 写起来是以下这样

export function fib(n: i32): i32 {
  var a = 0, b = 1
  if (n > 0) {
    while (--n) {
      let t = a + b
      a = b
      b = t
    }
    return b
  }
  return a
}

是不是跟 TypeScript 很像呢!?

AssemblyScript Simple Demo

既然都提到 AssemblyScript 了,就不免俗的玩玩看属於 AssemblyScript 的 Hello World Demo 吧!

首先建立一个空的资料夹,并在终端机里面执行

npm init -y
npm install --save @assemblyscript/loader
npm install --save-dev assemblyscript
npx asinit .

asinit 这个指令会帮我们在当前目录新建一个 AssemblyScript 的专案,我们接着就来看看这个指令帮我们建立了哪些档案与它们的用途为何

  ./assembly
  放置我们写的 AssemblyScript Source Code

  ./assembly/tsconfig.json
  一些配合 AssemblyScript 开发的 TypeScript Setting

  ./assembly/index.ts
  Compile 成 wasm 时的 entry file

  ./build
  存放 compile 後的 WebAssembly 档案的地方

  ./build/.gitignore
  排除一些 compile 後的 wasm 档案被上传到 Version Control Service  

  ./index.js
  Loading the WebAssembly module and exporting its exports.

  ./package.json
  你们懂的!!!

可以发现在 ./assembly/index.js 预设已经提供了一个简单的函式

export function add(a: i32, b: i32): i32 {
  return a + b;
}

接着我们就不做修改,直接编译它吧

npm run asbuild

回到 build 可以发现一些档案被生成了

接下来需要在要使用 WebAssembly Module 的地方进行 instantiation,WebAssembly Module 可以在 Node.js 也能在浏览器环境使用,差别在於 Node.js 可以使用 fs 的方式引入 wasm file,在 browser 则需要透过 network request 的方式载入。基本上 WebAssembly Module instantiation 有三种方式可以选择:

  • WebAssembly.Instance – Synchronous instantiation
  • WebAssembly.instantiate – Asynchronous instantiation
  • WebAssembly.instantiateStreaming – Asynchronous streaming instantiation

要注意 instantiateStreaming 目前在 Node.js 与 Safari browser 是尚未支援的(不过可以透过 polyfill 解决)

WebAssembly.instantiateStreaming(fetch('/build/optimized.wasm'), {})
  .then(wasmModule => {
    const exports = wasmModule.instance.exports;
    const mem = new Uint32Array(exports.memory.buffer);
  });

载入後就可以对 wasm module 作後续的操作罗!
使用方式大概像下面这样子,碍於篇幅就请有兴趣的读者自行研究罗~

// AssemblyScript

export function fibonacci(n: i32): i32 {
	let i: i32 = 1;
	let j: i32 = 0
	let k: i32;
	let t: i32;

	for (k = 1; k <= Math.abs(n); k++) {
	   t = i + j;
	   i = j;
	   j = t;
	}
	if (n < 0 && n % 2 === 0) {
		j = -j;
	}
	return j;
}


// Browser Side

const n = 1000;
const result = wasmModule.fibonacci(n);

Cache Wasm Module

在 web client side 使用 WebAssembly 的 module 时可以搭配快取将 compile 过後的 WebAssembly module 存起来,如此一来就不需要每次都得重新 download 与 compile,可以加快网站的性能。

然而一般我们比较熟悉的 browser storage 例如 localStorage 与 sessionStorage 是有大小限制的,WebAssembly module 很有可能超过这个限制,因此 MDN 官方是推荐使用一个你应该听过,却也应该没有用过的 Browser Database - IndexedDB 来快取 compile 过後的 WebAssembly Module。

对於使用 IndexedDB 快取 wasm module 有兴趣的读者可以阅读这篇 MDN 的文章,因为该篇文章已经写的十分完整,我就不在这里赘述。

(另外也推荐读者们使用 localForage 这个 library,可以让使用 browser storage 更为轻松方便。)

WebAssembly X Web Workers

经过昨天的文章後各位读者应该知道我们是有机会透过把一些任务丢到 Web Workers 中来实现平行处理的,而 WebAssembly 就是其中一种可能性。

把 WebAssembly 放到 Web Workers 来执行的好处是可以把 fetching, compiling and initialising 一个 WebAssembly module 的工作抽离 Main Thread,如果这个 WebAssembly 的 module 负责的是复杂运算与处理的操作,这麽做可以使页面效能提升不少。至於为什麽要将一些任务从 Main Thread 抽离与这麽做可能的优缺点都在上一篇文章说明过,如果不清楚的读者记得回头到上一篇回忆一下。

不过 WebAssembly 搭配 Web Workers 也是有缺点的,Main Thread 与 Workers 间的资料沟通是一个耗性能的操作,而且消耗的性能还会随着资料的大小而成长。再者 WebAssembly 搭配 Web Workers 也会使程序码变得更加复杂,也让程序与 wasm module 的互动变成非同步的形式(因为 message 的传递、Event Listener 与 Callback 等原因)。

所以说 WebAssembly 与 Web Workers 并不是一个绝配组合,实际上适不适合搭配使用还是得经过谨慎评估喔!有兴趣的读者可以参考这篇文章

Demo Time : React X WASM

要自己写 WebAssembly 并不容易,但至少我们可以学会怎麽使用别人写好的 WebAssembly 工具。

FFmpeg 是一个开放原始码的自由软件,可以执行音讯和视讯多种格式的录影、转档、串流功能,今天想要 demo 的是透过 FFmpeg 将影片转档成 gif 的功能。

(注意!这个 wasm module 因为使用到 sharedArrayBuffer,目前的浏览器预设都是关闭的,如果要能够顺利使用这个 module,必须设定 cross-origin isolated。不过我觉得就算跑不起来也没关系,因为这是一个简单的范例,目的是让各位读者了解使用 wasm module 并没有想像中那麽困难。)

在过去,要达成这样的功能一般来说会需要透过 client 与 server 沟通来达成,也就是前端将使用者上传的影片丢到後端,後端服务器跑 FFmpeg 进行转档後再吐回给前端。

这种方式主要会有两个问题:

  • 没有效率
  • offline 时没办法 work

幸好有了 WebAssembly 的出现,让我们有机会可以把这些操作拉到浏览器端来做。

首先使用 create-react-app 建立一个简单的 react 专案

npx create-react-app wasm-react-demo

接着下载 FFmpeg 的 WebAssembly Module

npm i @ffmpeg/ffmpeg @ffmpeg/core

再来看看 React Code 的部分

其实实作上并不难,建立 FFmpeg 的 instance 後就可以载入 wasm 的模组,比较值得一提的是可以看到 convertToGif 这个 function 中 ffmpegInstance 有跑一个 FS 的函数,这是因为 WebAssembly 会自己管理一个 in memory 的 file system ,所以可以执行像服务器端 readFile, writeFile 等操作。

如果想要优化这个范例,也可以试着尝试刚刚提过的搭配 Web Workers 或是 IndexedDB Cache 的方式,这就留给有兴趣的读者亲自尝试啦!

Demo Source Code: https://github.com/kylemocode/it-ironman-2021/tree/master/react-wasm-demo

(以上的 demo 建议读者可以去看看这部 Youtube 影片

本日小结

WebAssembly 的本意不是取代 JavaScript,尽管现在越来越多 WebAssembly 的框架出现,标榜着可以不用写 JavaScript 就能达到以前的功能,并且有着更好的效能,但不能忽略的是 WebAssembly 也有一些难题例如不容易 debug、档案大小过大...等等,且还是需要找到适合使用的时机,不然效能未必会比用 JS 写还要来得好(例如这篇文章就透过实验点出 WebAssembly 或是编译语言在某些状况下未必会比 JS 还要来得快)。

所以如果要给此时此刻的 WebAssembly 一个结论的话,我会认为它在短时间内不会取代 JavaScript,而更像是一种合作互补的关系。有了它 Web 开发又多了更多的可能性,而前端开发也是如此。我相信未来它的发展会越来越健全,身为前端开发者千万不能错过这波技术潮流,现在就开始关注它吧!

References & 图片来源

https://tigercosmos.xyz/post/2020/08/js/webassembly-intro/

https://www.youtube.com/watch?v=-OTc0Ki7Sv0&t=9s

https://developer.mozilla.org/zh-TW/docs/WebAssembly

https://www.sitepen.com/blog/getting-started-with-assemblyscript


<<:  Day30- Final Go

>>:  RDS程序开发

Typescript (tsconfig.json)

本系列文章经过重新编排和扩充,已出书为ECMAScript关键30天。原始文章因当时准备时程紧迫,...

资安制度建立前须蒐集的资料

一、充份授权 企业或机构应指派专责人员(Chief Information Security Off...

[Cmoney 菁英软件工程师战斗营] IOS APP 菜鸟开发笔记(7)----自定义弹出视窗

前言 因为UI和UX方面的需求,这几天上网搜寻了如何自定义下一页的弹出大小,弹出位置和动画,发现有蛮...

DAY19-网站构思之figma(一)

前言: 接下来就要进入网页构思的阶段了,在开始写网站之前,如同画画一样,一定都需要先打一个草稿,除...

DAY1 糟了!是世界奇观! 前言

前言 我是去年刚毕业与今年四月刚退伍的新鲜人。大学读的是资管系,在学期间没什麽写程序,没错,资管系毕...