イントロ
WebAssembly... とても速そうな響きである。
近年、HTML、CSS、JavaScriptに続きウェブ上で動作する新たな言語として注目されている。
WebAssembly、WasmはC++やRustからコンパイルし、JavaScriptよりも速く動作することを目的としている。
しかしどうやら2023年年末現在、どうやらWasmはDOMを直接扱えないらしい。WebGLなどのCanvasの描画にはWasmが使われるらしいがDOM自体となるとそうもいかないらしい。
とはいえフロントがカクついたりすると(書き方のせいであろうが)何かもっと動かないものだろうかと思ってしまう。
いや、オーバーヘッドがデカいだけならWasmでHTMLを書いてinnerHTMLにぶん投げれば速いのでは?
ここではRustを使ってWasmをやってみた。
Wasmのビルド
RustはMozzilaがやっているのでMDNを見ればわかりやすく書いてある。
-
wasm-pack のインストール
wasmをビルドするためにcargoでツールwasm-pack
を取ってくる。cargo install wasm-pack
-
wasm-bindgen の追加
JavaScriptとRustをつなぐためのwasm-bindgen
というクレートを追加する。cargo add wasm-bindgen
-
ビルド
いつものcargo build
ではなくwasm-pack
を使ってビルドする。wasm-pack build --target web
cargo build
ではバイナリがtarget
下に出力されるが、 webで使うものはpkg
に出力される。package.json
などもいるので他のディレクトリからnpm install './pkg'
などで見に行けば.d.ts
も吐き出してくれているし、簡単に使えるだろう。
innerHTMLする
Wasm
シンプルに以下のようなくそでかテーブルをinnerHTMLしよう
// lib.rs -> table_string.js
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn create_table() -> String {
let mut table = String::new();
table.push_str("<table><tbody>");
const SIDE: i32 = 256;
for i in 1..SIDE {
table.push_str("<tr>");
for j in 1..SIDE {
table.push_str(&format!("<td>{}</td>", (i - 1) * SIDE + j));
}
table.push_str("</tr>");
}
table.push_str("</tbody></table>");
table
}
MDNのように呼び出し側は
<head>
<script type="module" src="./main.js"></script>
</head>
<body>
<div id="contents"></div>
</body>
'use strict';
import init, { create_table } from '../../rust/pkg/table_string';
const main = async () => {
const contents: HTMLDivElement = document.getElementById('contents') as HTMLDivElement;
init().then(() => {
let ave = 0;
for(let i = 0; i < 100; i++){
const start: number = performance.now();
const t = create_table();
contents.innerHTML = t;
const end: number = performance.now();
const pastTime = end - start;
console.log(`Time: ${pastTime}`);
ave += pastTime;
}
console.log(`average: ${ave / 100}`);
});
return;
};
window.addEventListener('load', main);
とした。viteでbundleしたのでmain.jsだけとなっているが.wasmはimportされるので type="module"
が必要。
256x256のテーブルを作成し、100回の平均を取ったが速度は私の環境では
average: 140
となった。
速いのか? 比較せねばなるまい。
TypeScript
ここでも愚直にStringを作成しinnerHTMLを行った。
const create_table_by_inner_html = (): string => {
const SIDE = 256;
let table = '<table><tbody>';
for (let i = 1; i < SIDE + 1; i++) {
table += '<tr>';
for (let j = 1; j < SIDE + 1; j++) {
table += `<td>${(i - 1) * SIDE + j}</td>`;
}
table += '</tr>';
}
table += '</tbody></table>';
return table;
};
結果はこちら
average: 131
あまり速くない。というか全く速くない。
オーバーヘッドを考えれば少しは速いのかもしれないがオーダー的にメリットはないだろう。
innerHTMLは遅い
よく言われることであるが innerHTMLは遅いらしい。
DOMでやってみようか。
何回も createElement
したり威圧感としてはNo.1だろう
const create_table_by_dom = (): HTMLTableElement => {
const SIDE = 256;
const table: HTMLTableElement = document.createElement('table') as HTMLTableElement;
const tbody: HTMLTableSectionElement = document.createElement('tbody') as HTMLTableSectionElement;
for (let i = 1; i < SIDE + 1; i++) {
const row: HTMLTableRowElement = document.createElement('tr') as HTMLTableRowElement;
for (let j = 1; j < SIDE + 1; j++) {
const data: HTMLTableCellElement = document.createElement('td') as HTMLTableCellElement;
data.innerText = `${(i - 1) * SIDE + j}`;
row.appendChild(data);
}
tbody.appendChild(row);
}
table.appendChild(tbody);
return table;
};
結果は...
average: 123
やっぱり...見かけによらずこれが一番速いんですね...
まとめ
複雑なパースを行ったりなどしてDOMをつくるならもしかしたら速いのかもしれないが、
やはりDOMはWasmで触るものではないらしい。
画像処理やレンダリングなどオーバーヘッドが気にならない、DOM操作以外ならばWasmを使うなど適材適所使い分けていきたいですね。
WasmがDOMを直接操作できるようになるほうが嬉しいのではあるが。