コンテンツへスキップ

言語によるWebAssemblyファイルサイズの違い

WebAssembly自体はプログラミング言語ではありません。対応した各種言語から変換します。そのため、採用する言語によって利用できる機能や開発効率が異なるでしょう。

今回は生成されるバイナリをプログラミング言語ごとに見てみます。

GoとRust

利用したのはGoとRustです。RustはWebAssemblyで最初に採用されているプログラミング言語であり、サイズも最適化されているのではないかと予想されます。対してGoはWebAssemblyを目的としてはいませんし、標準ライブラリなども多数入りそうな予感がします。

どちらもHello Worldを出力するだけのWebAssemblyファイルを作った場合のサイズです。

  • Go : 1,301,957 byte
  • Rust : 1,981,246 byte

となり、意外にもGoのが小さくなります。

Rustのサイズを軽減する

Rustはデフォルトのままでは余計なコードがたくさん入っています。そこで wasm-gc を使ってサイズを軽減するのが基本です。このコマンドは下記コマンドでインストールできます。

view raw
index.
hosted with ❤ by GitHub

そしてコマンドを実行します。

wasm-gc target/wasm32-unknown-unknown/debug/hello.wasm hello.min.wasm

view raw
index.
hosted with ❤ by GitHub

この時生成される hello.min.wasm は207 byteとなっています。大幅に軽減されるのが分かります。

まとめ

Goはまだベータ実装なので、今後のバージョンアップによってサイズが最適化されていくのではないでしょうか。数MBあると、Webアプリケーションとしてはキャッシュの利用を考えたくなるでしょう。

Rustを使うことで小さなWebAssemblyファイルは作りやすくなります(もちろんコード量によりますが)。開発効率性をとるか、サイズをとるかは実行環境によるでしょう。よりシビアに動かす際にはプログラミング言語の選定にも注意が必要です。

インラインでWebAssemblyが使えるinline-wastを試す

WebAssemblyを使う場合、wasmという拡張子のファイルをサーバ上に配置して、それを読み込むのが一般的です。しかし何もファイルが必ず必要な訳ではありません。

そんな試みで作られているのがinline-wastです。WebAssemblyのテキスト版であるWASTを使ってインラインでWebAssemblyを使えるようにするソフトウェアです。

inline-wastの使い方

コード例です。i32.constなどがWASTにあたるコードです。

 

const {wastInstructions} = require('inline-wast/lib/interpreter');
function add(a, b) {
const fn = wastInstructions`
(i32.const ${a})
(i32.const ${b})
(i32.add)
`;
return fn();
}
console.log(add(3, 1)); // 4

view raw
index.js
hosted with ❤ by GitHub

ここで定義されるwastInstructionsの実行結果として返ってくる関数はWebAssemblyなので高速に処理されるものです。興味深いのは文字列で関数を生成しているので、動的にその内容を変更できるということです。処理が長くなりそうな場合、動的にWebAssembly化することで高速化させられる可能性があります。

WebAssemblyはバイナリとテキスト版があります。テキスト版は慣れれば読めないことはない文字列です。面白い仕組みです。

https://github.com/xtuc/inline-wast

TypeScriptからWebAssemblyを生成するAssemblyScriptを試す

WebAssemblyはプログラミング言語ではなく、様々なプログラミング言語から生成されるWebブラウザ上で実行できるバイナリフォーマットになります。基本はRustやC、C++であり、他にもGo、Java、Kotlin、Swiftなど様々なプログラミング言語が対応しています。

しかし複数のプログラミング言語を混ぜて開発するのは要員確保も大変です。そこで注目したいのがTypeScriptからWebAssemblyファイルを生成するAssemblyScriptです。今回はその特徴を紹介します。

JavaScriptの関数を呼び出せる

AssemblyScriptではdeclareを使って関数を定義することで、JavaScript側の関数を呼び出せます。例えば以下はsayHello関数を呼び出しています。

declare function sayHello(): void;
sayHello();

view raw
index.js
hosted with ❤ by GitHub

そしてJavaScript側では次のように記述します。mainの中でsayHelloという関数を定義することで、AssemblyScriptからJavaScriptの関数呼び出しを可能にしています。

WebAssembly.instantiateStreaming(fetch("../out/main.wasm"), {
main: {
sayHello() {
console.log("Hello from WebAssembly!");
}
},
env: {
abort(msg, file, line, column) {
console.error("abort called at main.ts:" + line + ":" + column);
}
},
})

view raw
index.js
hosted with ❤ by GitHub

JavaScriptからAssemblyScriptの関数を呼び出す

逆にJavaScriptからAssemblyScriptの関数を呼び出す場合はexportを使って関数を公開します。型を指定しなければなりません。

export function add(x: i32, y: i32): i32 {
return x + y;
}
export function minus(x: i32, y: i32): i32 {
return x y;
}

view raw
index.js
hosted with ❤ by GitHub

これで、WebAssemblyファイル読み込み後の result.instance.exports の中に関数が入ります。

exports.minus(23, 20);
// -> 3

view raw
index.js
hosted with ❤ by GitHub

文字列を返す場合

文字列の場合は多少注意が必要です。まず関数の定義の時点で文字列の長さも返すように定義します。

declare function sayHello(msg: string, len: usize): void;
export function say(): void {
const str: string = "hifive";
sayHello(str, str.length);
}

view raw
index.js
hosted with ❤ by GitHub

次に受け取った側ではメモリ中の値を読み取り、デコードしなければなりません。

sayHello(index) {
const length = mem.getUint32(index,true);
const array = new Uint16Array(mem.buffer,index + 4,length);
const str = new TextDecoder('utf-16').decode(array);
console.log(`Hello ${str}, from WebAssembly!`);
}

view raw
index.js
hosted with ❤ by GitHub

memはWebAssemblyファイルを読み込んだ後 result.instance.exports.memory.buffer で取得できます。

mem = new DataView(result.instance.exports.memory.buffer);

view raw
index.js
hosted with ❤ by GitHub

まとめ

WebAssemblyがTypeScriptで書けるようになれば、 TypeScriptだけでフロントエンドの開発が完結できます。もちろんサブセットなので全ての機能が使える訳ではありません。しかし高速な処理が必要になった際に、AssemblyScriptであれば書き慣れた構文でWebAssembly化できるメリットが大きいでしょう。

AssemblyScript/assemblyscript: A TypeScript to WebAssembly compiler 🚀

WebAssemblyファイルをデコンパイルする

WebAssemblyはあらかじめコンパイルされているのでJavaScriptのように実行時にコードをパースする必要がなく、高速に動作します。WebAssemblyのコードはバイナリデータになっているので処理を隠蔽できているように見えますが、デコンパイルを行うことで人間が読める形に変換できます。

今回はwabtを使ってWebAssemblyのデコンパイルを行ってみます。

元になるコード

今回はGoを使っています。一番簡単なコードです。

package main
func main() {
println("Hello, wasm!")
}

view raw
index.go
hosted with ❤ by GitHub

WebAssemblyにする

このコードをコンパイルしてWebAssemblyにします。

$ GOOS=js GOARCH=wasm go build -o test.wasm main.go

view raw
index.
hosted with ❤ by GitHub

そしてできあがるのが test.wasm です。このファイルは1.3MBありました。

デコンパイルする

ではここからデコンパイルします。その際に使うのがwabtです。WebAssembly用のツールが各種含まれています。ビルド時にはcmakeが必要です。

$ git clone –recursive https://github.com/WebAssembly/wabt
$ cd wabt
$ make

view raw
index.
hosted with ❤ by GitHub

そして生成されるwasm2watコマンドを使ってデコンパイルします。

bin/wasm2wat test.wasm > test.wat

view raw
index.
hosted with ❤ by GitHub

デコンパイルすると25.8MBになりました。かなり肥大化しているのが分かります。

コードを見る

デコンパイルされたコードは、WebAssemblyのテキスト版です。

(module
(type (;0;) (func (result i32)))
(type (;1;) (func (param i32)))
(type (;2;) (func (param i64 i64 i64 i64) (result i64)))
(type (;3;) (func (param i32 i32 i32) (result i32)))
(type (;4;) (func (param i64 i64 i64) (result i64)))
(type (;5;) (func (param i64 i64)))
(type (;6;) (func (param i32 i32)))
(type (;7;) (func (param i32 i32 i32)))
(type (;8;) (func (param i64 i64) (result i64)))
(type (;9;) (func (param f64) (result i64)))
(import "go" "debug" (func (;0;) (type 1)))
(import "go" "runtime.wasmExit" (func (;1;) (type 1)))
:

view raw
index.
hosted with ❤ by GitHub

その中に Hello, wasm という文字列も入っています。

byte block (GC sweep waitHello, wasm!\0abad map statefatal error:

view raw
index.
hosted with ❤ by GitHub

つまりWebAssemblyでバイナリ化していたとしても、任意の処理がどこで、どのように行われているか、探そうと思えば探し出せると言うことです。


WebAssemblyでコードをコンパイルしたとしても、100%安全という訳ではありません。Javaと同レベルくらいに考える方が良いかもしれません。隠し方は幾つかありますので、なるべく分かりづらくなる方法を選ぶべきで、安直にキーなどをコードに書かない方が良さそうです。

GoのWebAssemblyでネットワークを利用する

Go1.11からサポートされたWebAssemblyにおいて、ネットワークを利用する方法を紹介します。今回はJSONファイルの取得方法です。

注意点

実際のネットワークアクセスはWebブラウザの開発者ツール、ネットワークタブ内で確認できます。ネットワークアクセスを匿名化したり、CORS制限も適用されますので注意してください。

利用法

今回はCORS制限がなく、APIキーも不要なMicrolinkのコンテンツを取得します。 "net/http" パッケージを使ってネットワークアクセスをします。そして ioutil.ReadAll を使ってコンテンツを取得します。これはバイト文字列になっていますので、 string 関数を使って文字列化します。

package main
import (
"fmt"
"syscall/js"
"net/http"
"strconv"
"io/ioutil"
)
func main() {
resp, err := http.Get("https://api.microlink.io/?url=https://twitter.com/futurism/status/882987478541533189&video&palette")
if err != nil {
fmt.Println(err)
}
defer resp.Body.Close()
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println(err)
}
js.Global().Get("document").
Call("querySelector", "#html").
Set("value", string(b))
}

view raw
index.go
hosted with ❤ by GitHub


これまでのWebAssemblyはDOMやFetch APIなどが使えないのが難点でした。しかしGo版のWebAssemblyによって、その問題はなくなっています。 WebAssemblyの可能性を飛躍的に高めてくれるのではないでしょうか。

GoでWebAssemblyを使ってみよう

Go1.11からWebAssemblyが公式サポートされました。これまでWebAssemblyの開発はRustがメインに使われてきましたが、Goは構文も分かりやすく、非常に良い選択肢になると思われます。

特に面白いのはDOM連携がとても容易な点ではないかと思います。今回はその使い方を紹介します。

まず基本から

Go1.11以降のインストールが終わっている前提とします。例えばコンソールに出力するためには以下のようなコードを書きます。

package main
import (
"fmt"
)
func main() {
fmt.Println("Hello World")
}

view raw
index.go
hosted with ❤ by GitHub

これをコンパイルします。test.wasmというファイルが作成されます。

GOOS=js GOARCH=wasm go build -o test.wasm main.go

view raw
index.
hosted with ❤ by GitHub

Webブラウザで実行する際には、公式に用意されているHTML/JavaScriptファイルを使うと簡単です。

そしてWebサーバを立ち上げて wasm_exec.html を読み込みます。runというボタンがあるので、それを押すと処理が実行されます。

DOM連携

Goの場合、main関数を必ず使うようです。そしてmain関数は引数が使えません。そこで syscall/js パッケージを読み込みます。これを読み込むと、例えば #num1 の値を次のようにして取得できます。

js.Global().Get("document").
Call("querySelector", "#num1").
Get("value").
String()

view raw
index.go
hosted with ❤ by GitHub

逆にセットする場合は下記のようになります。

js.Global().Get("document").
Call("querySelector", "#res").
Set("value", "Hello")

view raw
index.go
hosted with ❤ by GitHub

Callはメソッドの呼び出し、値の取得はGet、設定はSetと非常にシンプルな形です。例えば足し算の処理は下記のように記述できます。

package main
import (
"fmt"
"syscall/js"
"strconv"
)
func main() {
num1 := js.Global().Get("document").
Call("querySelector", "#num1").
Get("value").
String()
i, _ := strconv.Atoi(num1)
fmt.Println(i)
num2 := js.Global().Get("document").
Call("querySelector", "#num2").
Get("value").
String()
j, _ := strconv.Atoi(num2)
js.Global().Get("document").
Call("querySelector", "#res").
Set("value", i + j)
}

view raw
index.go
hosted with ❤ by GitHub

これで足し算ができます。デモをこちらに置いてありますので体験してみてください。


なお、GoのWASMはサイズが若干大きめです(上記の足し算を行うもので2.5MBあります)。また、関数が使えない(分かっていないだけかも知れません)ので、ファイル数が増えたり、main関数での分岐が必要になるかも知れません。

とは言え、Goの文法でWASMが書けるメリットは非常に大きく、Webアプリケーションの高速化が容易に実現できたり、全体をGoで開発することも夢ではなさそうです。

WebAssemblyがDOM、APIに対応します!

WebAssemblyはWebアプリケーションを高速に処理できる技術ですが、幾つかの欠点もありました。その一つがDOMを処理できないこと、さらにFetch APIやWebAudioなどのAPIが使えない点です。

しかし先日Announcing the web-sys crate! | Rust and WebAssemblyがアナウンスされ、WebAssemblyでもDOMや各種APIの利用が可能となっています。

実際にはラッピング?

今回発表されたのはweb-sysというクレート(パッケージ)です。すでにWebAssemblyに対応しているWebブラウザであれば利用可能となっていることから、WebAssemblyエンジン自体のバージョンアップは不要なようです。そのため、DOMやAPI操作はメインスレッド側のJavaScriptで行っているかと思われます。ただし、Fetch APIで使ったURLなどはWebブラウザ側のソースにはありませんでした(ネットワークログを見ると、どこにアクセスしたかは分かります)。

Hello World

Hello Worldのアラートを出す場合のコードです。

extern crate wasm_bindgen;
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
extern "C" {
fn alert(s: &str);
}
#[wasm_bindgen]
pub fn greet(name: &str) {
alert(&format!("Hello, {}!", name));
}

view raw
index.rs
hosted with ❤ by GitHub

DOM操作

DOMに記述する際のコードです。 web_sys クレートを介してWindowやDocumentにアクセスできます。

extern crate wasm_bindgen;
extern crate web_sys;
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn run() -> Result<(), JsValue> {
let window = web_sys::window().expect("no global `window` exists");
let document = window.document().expect("should have a document on window");
let body = document.body().expect("document should have a body");
let val = document.create_element("p")?;
val.set_inner_html("Hello from Rust!");
AsRef::<web_sys::Node>::as_ref(&body).append_child(val.as_ref())?;
Ok(())
}

view raw
index.rs
hosted with ❤ by GitHub

Fetch API

Fetch APIを使って外部リソースを取得するコードです。Promiseを使います。

extern crate futures;
extern crate js_sys;
extern crate wasm_bindgen;
extern crate wasm_bindgen_futures;
extern crate web_sys;
#[macro_use]
extern crate serde_derive;
use futures::{future, Future};
use js_sys::Promise;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use wasm_bindgen_futures::future_to_promise;
use wasm_bindgen_futures::JsFuture;
use web_sys::{Request, RequestInit, RequestMode, Response};
#[derive(Debug, Serialize, Deserialize)]
pub struct Branch {
pub name: String,
pub commit: Commit,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Commit {
pub sha: String,
pub commit: CommitDetails,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct CommitDetails {
pub author: Signature,
pub committer: Signature,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Signature {
pub name: String,
pub email: String,
}
#[wasm_bindgen]
pub fn run() -> Promise {
let mut opts = RequestInit::new();
opts.method("GET");
opts.mode(RequestMode::Cors);
let request = Request::new_with_str_and_init(
"https://api.github.com/repos/rustwasm/wasm-bindgen/branches/master&quot;,
&opts,
).unwrap();
request
.headers()
.set("Accept", "application/vnd.github.v3+json")
.unwrap();
let window = web_sys::window().unwrap();
let request_promise = window.fetch_with_request(&request);
let future = JsFuture::from(request_promise)
.and_then(|resp_value| {
assert!(resp_value.is_instance_of::<Response>());
let resp: Response = resp_value.dyn_into().unwrap();
resp.json()
}).and_then(|json_value: Promise| {
JsFuture::from(json_value)
}).and_then(|json| {
let branch_info: Branch = json.into_serde().unwrap();
future::ok(JsValue::from_serde(&branch_info).unwrap())
});
future_to_promise(future)
}

view raw
index.rs
hosted with ❤ by GitHub


rustwasm/wasm-bindgen: Facilitating high-level interactions between wasm modules and JavaScriptに多数のサンプルが掲載されていますので、ぜひコードを見てください。動いているコードはExamples – The wasm-bindgen Guideにて確認できます。WebGLやCanvas、WebAudioなどのサンプルが掲載されています。

WebAssemblyはどれくらい速いのか

WebAssembly(WASM)はWebブラウザ上で動くプログラムです。JavaScriptとの違いはコンパイルされた実行ファイルであることで、JavaScriptのようにパースする必要がないので高速に処理されます。

高速とは言っても、どれくらい速いのか実行してみないと分からないでしょう。そこで試した結果を紹介します。

フィボナッチ関数を実行する

フィボナッチ関数はJavaScriptで書くと次のように表現されます。

function f(n) {
if (n <= 1) return 1;
return f(n 1) + f(n 2);
}

view raw
index.js
hosted with ❤ by GitHub

例えば5のフィボナッチ数は8、6のフィボナッチ数は13になります。小さな数であれば一瞬で求められますが、40くらいの数字になると時間がかかるようになります。同じ関数をRustで書くと次のようになります。

pub fn f(n:i32) -> i32 {
if n <= 1 {
1
} else {
f(n 1) + f(n 2)
}
}

view raw
index.rs
hosted with ❤ by GitHub

計算する

では実際に計算を行ってみた結果です(macOS Safariでの実行結果。ハードウェアの性能によって数値は異なります)。単位はmsです。

  JavaScript WASM
20 2 1
30 69 4
40 6,733 423
42 17,356 1,061
44 46,195 2,839

この結果から分かるのは、少ない数であればJavaScriptもWebAssemblyも殆ど変わりませんが、40以上になるとWebAssemblyが圧倒的に高速になります。50になるとJavaScript側で処理するのは現実的ではないでしょう。フィボナッチ関数に限らず、リストのソート処理であったり、ループ処理などはWebAssemblyの方が効率的なはずです。


WebAssemblyではネットワーク関数が使えない、DOMが扱えないと言った制限がありますが(現在解消されようとしています)、計算処理の速度においては使わない手はありません。開発できる言語はRustに限らず、様々な言語があります。ぜひWebAssemblyの活用を検討してください。

PWAハンズオンを開催しました

PWA(Progressive Web Apps)というキーワードに注目が集まっていますが、実際に取り組む機会がないという方は多いようです。そこでhifiveのTodoアプリをベースに、PWAを体験してもらうハンズオンを開催しました。

コンテンツはGitHubにあります

体験してもらったハンズオンコンテンツはhifivemania/pwahandsondocsに置いてあります。内容としてはこれに合わせて進めたので、オンラインで体験してもらうことも可能です。ハンズオンはこの資料を基に各自のペースで進めてもらうのですが、何か分からないところがあればすぐにフォローしてもらえるのが参加メリットになります。

hifiveとは

ハンズオンの最初にhifiveとは何かを紹介しました。今回のハンズオンではTodo投稿機能などにおいてhifiveを使っていますが、PWAを扱う上では必須ではありません。しかしビジネスとしてWebシステムを開発する上で問題になりがちな要員確保や中長期的なメンテナンスという問題においてhifiveは最適な解決策になるべく開発を進めています。これはPWAにおいて役立つ場面があるはずです。

hifive – HTML5企業Webシステムのための開発プラットフォーム – hifive

ハンズオン開始

ハンズオンは前述の通り、各自のペースで行ってもらいます。そのため、ハンズオン中は基本的に静かで黙々と作業されています。進めていく中でつまずくポイントは幾つも、人によっても異なるのでそれらを随時フォローアップしていきました。

今回体験してもらったもの

今回のハンズオンではPWAにおいて以下の要素を体験してもらいました。

  • オンライン/オフライン対応
  • アプリ化
  • Webプッシュ通知

PWAでは他にも要素技術があります。今回のを基礎として、自社やクライアント向けの開発に役立てて欲しいと思います。

次回開催について

PWAハンズオンは好評いただいており、すでに2回目が設定済みです。会場は今回と同じく渋谷のhoops link tokyoさんになります。日時は4月12日(金)19時からです。PWAを体験してみたい方はぜひご参加ください!

PWAハンズオン – connpass

Workboxのプラグインについて

PWAにおいて肝となるのがキャッシュです。あるURLにアクセスした時にコンテンツを返却するのがキャッシュですが、HTML5におけるCACHE APIはすべてプログラムから操作しなければならず、実装が大変です。

そこで使ってみたいのがWorkboxになります。Google製のPWAにおけるキャッシュコントロールを便利にしてくれるライブラリになります。今回はWorkboxに対応しているプラグインと、その機能を紹介します。

workbox.backgroundSync.Plugin

ネットワークがオフラインだった場合、オンラインになったタイミングで同期してくれます。queueArgsとしてキューに送信する引数を指定します。

workbox.broadcastUpdate.Plugin

キャッシュを更新した時に、それをメッセージングしてくれます。メインスレッドのJavaScriptで受け取ったならば、表示を更新したりユーザに再読込を促したりできるでしょう。チャンネル名を指定します。

オプションは以下の通りです。

  • headersToCheck
    チェックするヘッダーを指定。
  • source
    データ元のソースを指定。

workbox.cacheableResponse.Plugin

キャッシュする際のヘッダーレスポンスを指定。

オプションは以下の通りです。

  • statuses
    キャッシュするHTTPレスポンスコードを配列で指定。
  • headers
    キャッシュするHTTPレスポンスヘッダーをオブジェクトで指定。

workbox.expiration.Plugin

キャッシュに有効期限を付けられるプラグイン。

オプションは以下の通りです。

  • maxEntries
    最大の登録数。
  • maxAgeSeconds
    有効期限。秒で指定。
  • purgeOnQuotaError
    容量制限に引っかかった場合はエラーにするかどうか。

workbox.rangeRequests.Plugin

HTTPのRangeへのアクセス対応用です。分割ファイルをリクエスト、キャッシュするのに使います。

自作プラグインの作り方

プラグインは以下の5つのメソッドを持ったオブジェクトになります。

  • cacheWillUpdate
  • cacheDidUpdate
  • cachedResponseWillBeUsed
  • requestWillFetch
  • fetchDidFail

Workboxを使えばキャッシュのコントロールが柔軟になります。プラグインを使ったり、自作することでより簡単に実装できるようになるでしょう。ぜひ使い方を覚えましょう。

Workbox | Google Developers