Wasmを触ってみた ~DOM操作の夢を見る~

# イントロ

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を見ればわかりやすく書いてある。

https://developer.mozilla.org/ja/docs/WebAssembly/Rust_to_wasm

1. wasm-pack のインストール
    wasmをビルドするためにcargoでツール `wasm-pack` を取ってくる。
    ```shell
    cargo install wasm-pack
    ```
    
2. wasm-bindgen の追加
    JavaScriptとRustをつなぐための `wasm-bindgen` というクレートを追加する。
    ```shell
    cargo add wasm-bindgen
    ```
    
3. ビルド
    いつもの `cargo build` ではなく `wasm-pack` を使ってビルドする。
    ```shell
    wasm-pack build --target web
    ```

    `cargo build` ではバイナリが `target` 下に出力されるが、 webで使うものは `pkg` に出力される。

    `package.json` などもいるので他のディレクトリから `npm install './pkg'` などで見に行けば `.d.ts` も吐き出してくれているし、簡単に使えるだろう。

# innerHTMLする

## Wasm
シンプルに以下のようなくそでかテーブルをinnerHTMLしよう

```rust
// 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のように呼び出し側は

```html
<head>
	<script type="module" src="./main.js"></script>
</head>
<body>
	<div id="contents"></div>
</body>
```

```ts
'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"` が必要。

大きなテーブルの完成である。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3485638/fdf65aa7-f318-bf28-253a-c85b57cede90.png)

256x256のテーブルを作成し、100回の平均を取ったが速度は私の環境では
![image_wasm.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3485638/392d173a-071a-bf19-f2d5-b88869778158.png)

```text
average: 140
```

となった。
速いのか? 比較せねばなるまい。

## TypeScript

ここでも愚直にStringを作成しinnerHTMLを行った。

```ts
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;
};
```

結果はこちら

![image_jsstr.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3485638/6d5477b9-6944-b89f-5806-6625ed53e10a.png)

```text
average: 131
```

あまり速くない。というか全く速くない。

オーバーヘッドを考えれば少しは速いのかもしれないがオーダー的にメリットはないだろう。

# innerHTMLは遅い

よく言われることであるが innerHTMLは遅いらしい。
DOMでやってみようか。
何回も `createElement` したり威圧感としてはNo.1だろう

```ts
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;
};
```

結果は...

![image_jsdom.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3485638/0d33e10e-d0ce-8cab-2934-c6b52077c0aa.png)

```text
average: 123
```

やっぱり...見かけによらずこれが一番速いんですね...

# まとめ

複雑なパースを行ったりなどしてDOMをつくるならもしかしたら速いのかもしれないが、
やはりDOMはWasmで触るものではないらしい。

画像処理やレンダリングなどオーバーヘッドが気にならない、DOM操作以外ならばWasmを使うなど適材適所使い分けていきたいですね。

WasmがDOMを直接操作できるようになるほうが嬉しいのではあるが。