🧪

処理速度比較: JavaScript VS Wasm(Pyodide)

2025/04/15に公開

1. はじめに

近年、ブラウザ上での高性能な処理を実現するためにWebAssembly(Wasm)が注目されています。特にPythonをブラウザ上で実行できるPyodideなどのフレームワークの登場により、データ分析や機械学習などの複雑な計算をクライアントサイドで実行できるようになりました。しかし、すべてのケースでWasmが優れているわけではありません。

この記事では、JavaScript(ネイティブ)とWebAssembly(Python/Pyodide)の処理速度を、タスクの複雑度別に比較した検証結果を紹介します。

2. 検証環境

今回の検証では以下の環境とツールを使用しました。

  • フロントエンド基盤: HTML/CSS/JavaScript
  • WebAssembly実行環境: Pyodide
    • Pyodideとは: WebAssembly上でPythonとそのライブラリを実行できるフレームワーク
  • 機械学習ライブラリ:
    • JavaScript側: TensorFlow.js
    • Python側: scikit-learn
  • ブラウザ環境: Chrome

3. 検証内容

処理の複雑さを3段階に分け、JavaScriptとWebAssembly(Python)の性能を比較しました。各段階で実行時間を10回ずつ測定し、平均値を算出しています。

1. 低複雑度タスク

  • 単純なループ処理(0〜100万までの数値加算)
  • 基本的な計算性能の比較

2. 中複雑度タスク

  • アルゴリズム実装(エラトステネスのふるい法による素数列挙)
  • 10万以下の素数を判定

3. 高複雑度タスク

  • 機械学習処理(1000サンプルの線形回帰モデル学習)
  • 実践的なデータ処理シナリオの評価

各タスクで以下の指標を測定しました。

  • 総実行時間
  • Python/Wasm内部実行時間
  • Wasm初期ロード時間のオーバーヘッド

4. 検証結果

1. ループ処理テスト(低複雑度)

カテゴリ 平均処理時間(秒)
JavaScript 0.00397
Wasm(Python) 0.15224
うちPython内部実行時間 0.1401
うちWasm初期ロード時間 0.01214

ポイント: JavaScriptがWasm(Python)より約38倍高速です。単純な計算処理ではネイティブJavaScriptの方が圧倒的に優位です。

2. 素数判定テスト(中複雑度)

カテゴリ 平均処理時間(秒)
JavaScript 0.00498
Wasm(Python) 0.03116
うちPython内部実行時間 0.0185
うちWasm初期ロード時間 0.01266

ポイント: JavaScriptがWasm(Python)より約6倍高速です。アルゴリズム実装においても、中程度の複雑さではJavaScriptが優位を維持しています。

3. 機械学習テスト(高複雑度)

カテゴリ 平均処理時間(秒)
JavaScript 3.77999
Wasm(Python) 2.85027
うちPython内部実行時間 0.0072
うちWasm初期ロード時間 2.84307

ポイント: Wasm(Python)がJavaScriptより約25%高速です。Python内部の実行時間は非常に短く、ほとんどの時間がWasmの初期ロードに費やされています。しかし、全体としてはPythonの科学計算ライブラリの最適化が効果を発揮し、JavaScriptよりも高速に処理できています。

5. 実装の詳細

テストはシンプルなHTMLページ上で実行され、各処理の時間計測を行いました。以下に各タスクの実装概要を示します。

ループ処理の実装(低複雑度)

JavaScript版:

let start = performance.now();
let sum = 0;
for (let i = 0; i < 1000000; i++) {
    sum += i;
}
let end = performance.now();
let timeTaken = (end - start) / 1000;

Python+Wasm版:

import time
start_time = time.time()
sum = 0
for i in range(1000000):
    sum += i
end_time = time.time()
(sum, end_time - start_time)

素数判定の実装(中複雑度)

両方のバージョンでエラトステネスのふるい法を使用しています。

JavaScript版:

function findPrimes(max) {
    const sieve = new Array(max + 1).fill(true);
    sieve[0] = sieve[1] = false;
    const primes = [];
    
    for (let i = 2; i <= max; i++) {
        if (sieve[i]) {
            primes.push(i);
            for (let j = i * i; j <= max; j += i) {
                sieve[j] = false;
            }
        }
    }
    
    return primes;
}

Python+Wasm版:

def find_primes(max_num):
    sieve = [True] * (max_num + 1)
    sieve[0] = sieve[1] = False
    primes = []
    
    for i in range(2, max_num + 1):
        if sieve[i]:
            primes.append(i)
            for j in range(i * i, max_num + 1, i):
                sieve[j] = False
    
    return primes

機械学習の実装(高複雑度)

JavaScript版: TensorFlow.jsを使用した線形回帰モデル

// データ生成
const numSamples = 1000;
const trueWeight = 2.5;
const trueBias = -0.5;
const xs = tf.randomUniform([numSamples], 0, 10);
const noise = tf.randomNormal([numSamples], 0, 0.1);
const ys = xs.mul(trueWeight).add(trueBias).add(noise);

// 標準化
const xsMean = xs.mean().dataSync()[0];
const xsStd = Math.sqrt(xs.sub(xsMean).square().mean().dataSync()[0]);
const xsNormalized = xs.sub(xsMean).div(xsStd);

// モデル定義と訓練
const model = tf.sequential();
model.add(tf.layers.dense({units: 1, inputShape: [1], useBias: true}));
model.compile({optimizer: tf.train.sgd(0.1), loss: 'meanSquaredError'});
await model.fit(xsNormalized, ys, {epochs: 100, batchSize: 32});

Python+Wasm版: scikit-learnを使用した線形回帰モデル

import numpy as np
from sklearn.linear_model import SGDRegressor
from sklearn.preprocessing import StandardScaler

# データ生成
np.random.seed(42)
X = np.random.uniform(0, 10, num_samples).reshape(-1, 1)
noise = np.random.normal(0, 0.1, num_samples)
y = X.flatten() * true_weight + true_bias + noise

# データのスケーリング
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

# モデル定義と訓練
model = SGDRegressor(learning_rate='constant', eta0=0.1, max_iter=100)
model.fit(X_scaled, y)

6. 考察:タスク複雑度による最適技術選択

検証の結果から、以下の重要なポイントが明らかになりました。

1. シンプルな処理では JavaScript が圧倒的に高速

  • ループ処理では約38倍、素数判定でも約6倍の速度差
  • 低〜中複雑度の計算処理ではJavaScriptを選ぶべき

2. 複雑な機械学習処理では Wasm(Python) が優位

  • Pythonの科学計算ライブラリ(scikit-learn)の最適化が効果的
  • 高度な数値計算や統計処理を伴う場合はPython+Wasmが適している

3. Wasm の初期ロード時間が全体のパフォーマンスに大きく影響

  • 機械学習テストでは、Python内部実行時間は非常に短いが初期ロード時間が長い
  • 一度ロードしたWasmを繰り返し使う場合、この初期コストは償却される

7. 技術選択の指針

この検証結果から、Webアプリケーション開発における技術選択の指針として以下が提案できます:

  1. 単純な数値計算や基本的なデータ処理:JavaScriptを使用する

    • パフォーマンスが重要な単純な反復処理
    • ページのレスポンス時間が重要な場合
  2. 複雑な数学的計算や統計処理、機械学習:Wasm(Python)を検討する

    • 高度なデータ分析や機械学習モデルの実行
    • 科学計算ライブラリを活用する処理
  3. 頻繁に実行されるか一度きりか

    • 初回実行コストを償却できる繰り返し処理ではWasmの利点が活きる
    • 一度きりの処理ではロードオーバーヘッドを考慮する必要がある

8. まとめ

WebAssembly(Wasm)は高度な計算処理で優位性を発揮する一方、シンプルな処理ではネイティブJavaScriptの方が効率的であることが明らかになりました。アプリケーションの要件やタスクの複雑さに応じて適切な技術を選択すべきです。

特に注目すべきは、Python+Wasm構成では初期ロード時間が大きなオーバーヘッドとなる点です。しかし、繰り返し使用されるケースや、複雑な数値計算を必要とするシナリオでは、このオーバーヘッドを上回るメリットが得られる可能性があります。

適材適所で技術を選ぶことで、Webアプリケーションのパフォーマンスを最大化できると考えられます。

参考

https://zenn.dev/jnch/articles/34c97cc5c7a1a7

Discussion