Rust

Rust製JavaScriptエンジンBoaとは|boa_engine 0.22の導入と性能実測

Rust製JavaScriptエンジンBoaとは|boa_engine 0.22の導入と性能実測

Boaは、Rustだけで書かれた組み込み向けのJavaScriptエンジンです。Rustのプログラムに「利用者が書いたJavaScriptを実行させる口」を付けたいときの選択肢になります。ただし公式リポジトリは自らを experimental と書いており、V8やQuickJSの置き換えとして選ぶものではありません。2026年8月に出た最新版 boa_engine 0.22.0 を実際にビルドし、記事中のコードと新機能の動作、速度を手元で測った結果をもとに、導入手順と採用を見送るべき条件を整理します。

まとめ

  • 最新版は boa_engine 0.22.0(2026-08-28公開)。Rust 1.91.0以上が必要で、Temporal(新しい日付API)が既定で有効になった。ライセンスは Unlicense OR MIT。
  • ECMAScript準拠度は0.22.0時点で95.60%(公表値)。mainブランチは2026-10-31時点で 53,578件中51,450件がpass、fail 480件。
  • 速度はJITを持たないインタプリタ相当。公式ベンチマークのスコアは Boa 223に対しQuickJS 1,226、V8(JIT無効)2,561 で、計測対象の中で最下位。手元の再帰フィボナッチでもNode.js(JIT無効)の約8倍遅かった。
  • console.log や setTimeout はエンジン本体に無い。0.22では boa_wintertc を登録すれば使えるが、fetch・URL・AbortController は0.22.0の同クレートでは未実装で、従来の boa_runtime が要る。
  • 無限ループや深い再帰は runtime_limits_mut() でRust側から打ち切れる。この打ち切りはJavaScript側の try...catch では捕捉できない(0.22.0で実測)。

Boa(boa_engine)の現在地

Boaは boa-dev コミュニティ(公式サイトは boajs.dev)が開発しているオープンソースのJavaScriptエンジンで、字句解析・構文解析・バイトコードVMをすべてRustで実装しています。crates.ioの boa_engine は2022年3月に登録され、2026年11月1日時点の累計ダウンロードは約599万、直近90日で約190万、boa_engine に依存するクレートは103本です。ライセンスは Unlicense OR MIT で、いずれかを選択できます。商用利用・改変・再配布は可能ですが、MITを選ぶ場合は著作権表示と許諾文の同梱が必要です。

リリース頻度は一定ではありません。2024年はパッチ版を含めて0.18.0から0.20.0まで4本出た一方、2025年は0.21.0(10-21)の1本、2026年は0.21.1(03-29、パッチ)と0.22.0(08-28)が出ました。0.x系が続いており、APIは安定版に達していません。長く運用するプロダクトに組み込むなら、マイナー版ごとの破壊的変更を前提に工数を見積もってください。Rust側の基礎はRust入門|環境構築から所有権・基本文法、受託開発での採用判断までで確認できます。

Boa 0.22.0の既定機能とVMの変更点

v0.22のリリースノートが挙げる主な変更は次の5点です。

  • Temporal が既定で有効になった(boa_engine の default フィーチャが float16・xsum・temporal の3つになった)
  • インラインキャッシュが単相から多相(Polymorphic Inline Cache)になり、バイトコードとVMの最適化が入った
  • Web API層を boa_runtime から新クレート boa_wintertc へ移し始めた
  • エンジン内部の panic を PanicError に置き換える作業が始まった
  • RegExp.escape()、FinalizationRegistry、Iterator.concat()、Iterator Helpers、Error.prototype.stack などの組み込みが追加された

リリースノートの「New builtins」には Iterator.prototype.includes() と Promise.allKeyed() も載っていますが、この2つは既定のビルドでは使えません。0.22.0を既定フィーチャで組んで typeof を取ると、どちらも "undefined" でした。ソースを見ると両者は #[cfg(feature = "experimental")] の下にあり(includes は提案のStage 2.7)、features = ["experimental"] を付けて組み直すと "function" に変わります。リリースノートを読んで使い始める前に、フィーチャ指定を確認してください。

エンジンとランタイムの境界

Boaのエンジン本体が担うのはECMAScriptの実行だけです。0.22.0の既定ビルドで調べると、console・setTimeout・fetch・atob・structuredClone はすべて undefined でした。これらはECMAScript仕様の外にあるWeb APIで、別クレートから登録します。国際化APIの Intl も既定では入っておらず、標準のICU4Xデータを同梱して使うなら intl_bundled を有効にします。独自のデータプロバイダーを用意する場合は intl も選べます。

エンジンとランタイムの切り分けはV8とNode.jsの関係と同じです。エンジン側の実行単位の考え方はV8 Isolateとは?Contextとの違い・起動時間の実測とCloudflareの分離設計【2026年9月】で整理しています。Boaが新しく揃えにいっているのは、サーバー側JavaScriptランタイム共通の最小API群(WinterTCのMinimum Common Web API)で、その仕様の位置づけはWinterTCとは?WinterCGから移管したEcma TC55とECMA-429の要点にまとめています。

クレートは機能ごとに分かれていて、通常は boa_engine だけを依存に入れます。2022年以前の記事にある boa = "0.x" という指定は旧クレートで、crates.io上でも「DEPRECATED. Use the boa_engine crate instead.」と表示されています(最終版は2022年9月の0.13.1)。

Test262の実カウントによるECMAScript準拠度

BoaはECMAScript公式テストスイート(Test262)の結果を boa-dev/data リポジトリに毎日公開しています。0.22.0タグ時点とmainブランチ最新(2026-10-31更新)の生データは次のとおりです。

区分 v0.22タグ main(2026-10-31)
テスト総数 53,578 53,578
pass 51,225 51,450
ignored(対象外) 1,781 1,648
panic 0 0
fail 572 480
pass ÷ 総数 95.61% 96.03%

準拠度の式は「pass ÷ 総数(ignored込み)」です。v0.22タグの値が95.61%となり、リリースノートの公表値95.60%とほぼ一致することで確かめられます。0.20の公表値は89.92%、0.21は94.12%だったので、約2年で6ポイント近く上がりました。panicが0件なのはTest262の範囲でハーネスが異常終了しなかったという意味で、悪意のある入力への堅牢性を保証するものではありません。信頼できない入力を扱うなら、後述の実行上限と組み合わせてください。最新の値は公式のconformanceページで確認できます。

速度の実測位置|公式ベンチマークとNode.jsとの比較

「Rust製だから軽量で高速」という説明を国内の解説記事で見かけますが、実測値はそれを支持していません。

公式ベンチマークのスコア

boa-dev/data の bench には、V8ベンチマークスイート(Richards・DeltaBlue・Crypto・RayTrace・EarleyBoyer・RegExp・Splay・NavierStokesの8種)を各エンジンで毎日走らせた結果が入っています。スコアは高いほど速く、2026-10-31計測分は次のとおりです。公式ジョブはBoaのmainブランチをビルドしており、この表は0.22.0リリース版に固定した比較ではありません。

エンジン スコア 実装言語
Porffor 2,602 JavaScript(AOT)
V8(JIT無効) 2,561 C++
QuickJS 1,226 C
SpiderMonkey(JIT無効) 775 C++
Kiesel 412 Zig
Duktape 373 C
Boa 223 Rust

V8とSpiderMonkeyはJITを切った条件で、Porfforは事前コンパイル方式なので前提が違います(SerenityOS由来のlibjsは2026年1月以降の計測値がありません)。それを差し引いても、BoaはV8(JIT無効)の約11分の1、組み込み用途で競合するQuickJSの約5分の1です。同じ系列の2024-07-07時点のスコアは81だったので、2年余りで約2.8倍になりました。それでも、現時点で遅いことに変わりはありません。

手元での実測:再帰フィボナッチと起動時間

ベンチマークの表だけでは体感がつかみにくいので、同じ関数を手元で比べました。環境は Intel Core i9-9880H のmacOS、boa_engine 0.22.0 のreleaseビルドと Node.js v26.5.0 です。fib(27)(結果196418)を1回の起動で3回走らせ、その最速値を取りました。Boaは2回起動して測ったので、2つの値を載せています。

実行系 fib(27) 3回中の最速
Node.js v26.5.0(JIT有効) 6.2ms
Node.js v26.5.0(–jitless) 56.3ms
Boa 0.22.0(2回起動) 465ms/546ms

BoaはJITを切ったV8の約8倍、JITが効くV8の約80倍かかりました。一方、Context::default() の生成は1.3〜1.7msで、短いスクリプトをその都度新しいコンテキストで評価する場合は、この生成時間を呼び出し頻度と応答時間の予算に加えて判断してください。最小のeval例をreleaseビルドした実行ファイルは約13.1MB(13,149,304バイト、strip前)でした。依存クレートのコンパイルを含むクリーンビルドには、16スレッドのこの環境で約4分かかりました。

処理件数や応答時間の要件が厳しい用途では、対象スクリプトで必要性能を満たすと確認できない限り、Boaの採用を見送るべきです。判断軸は次の章以降で見る「Rustプロセスの中で何を完結させたいか」になります。

boa_engineの導入とRustからのJavaScript実行

Cargo.tomlへの追加と最低Rustバージョン

導入は依存を1行足すだけです。利用者がBoa用のCコンパイル手順やbuild.rsを書く必要はなく、依存を追加してcargoでビルドできます。

[dependencies]
boa_engine = "0.22.0"

0.22.0の最低Rustバージョンは 1.91.0 で、Boa自身は edition 2024 を使用しています(crates.io の rust_version 欄)。0.21系の1.88.0から上がったので、CIやDockerイメージのツールチェーンが古いとここで止まります。Rust本体の最近の変更はRust 1.97.0とは|v0シンボルマングリング既定化とCargo警告制御の変更点【2026年7月】、依存管理の基本操作はCargo(Rust)とは?インストールとcargo installの使い方・主要コマンド一覧にまとめています。

最小構成でのスクリプト評価

実行の起点は Context です。公式READMEの最小例は0.22.0でもそのまま動きます。

use boa_engine::{Context, Source, JsResult};

fn main() -> JsResult<()> {
    let js_code = r#"
        let two = 1 + 1;
        let definitely_not_four = two + "2";
        definitely_not_four
    "#;

    let mut context = Context::default();
    let result = context.eval(Source::from_bytes(js_code))?;
    println!("{}", result.display());

    Ok(())
}

実行すると "22" と表示されます。数値の4ではなく文字列の “22” になるのは、JavaScriptの暗黙の型変換がそのまま再現されているためです。

JavaScriptから呼び出すRust関数の登録

実用上の本題は、ホスト側の機能をJavaScriptへ渡すことです。register_global_callable にクロージャを登録すると、グローバル関数としてスクリプトから呼べます。

use boa_engine::{
    Context, JsResult, JsString, JsValue, Source,
    native_function::NativeFunction,
};

fn main() -> JsResult<()> {
    let mut context = Context::default();
    let budget = 255;

    context.register_global_callable(
        JsString::from("remaining_budget"),
        0,
        NativeFunction::from_copy_closure(move |_, _, _| {
            Ok(JsValue::new(budget))
        }),
    )?;

    let value = context.eval(Source::from_bytes("remaining_budget()"))?;
    println!("{}", value.display());

    Ok(())
}

0.22.0で実行すると 255 が返ります。この例では整数の設定値をクロージャに取り込みます。使用しているfrom_copy_closureはCopyなクロージャに限られるため、任意の所有型やハンドルを同じ方法で取り込めるわけではありません。Rustの構造体をJavaScriptのクラスとして公開する例は、v0.22タグの examples にある classes.rs が出発点になります。

boa_wintertcによるconsole.logとsetTimeoutの追加

前述のとおり、エンジン単体では console.log すら使えません。0.22では boa_wintertc = "0.22.0" を依存に足し、register を1回呼べば揃います。

use boa_engine::{Context, Source, JsResult};

fn main() -> JsResult<()> {
    let mut context = Context::default();
    boa_wintertc::register(None, &mut context)?;
    context.eval(Source::from_bytes(r#"
        console.log("btoa:", btoa("boa"));
        console.log(typeof setTimeout, typeof queueMicrotask, typeof structuredClone);
        console.log(typeof AbortController, typeof TextEncoder, typeof fetch, typeof URL);
        setTimeout(() => console.log("timer fired"), 0);
        Promise.resolve().then(() => console.log("microtask"));
    "#))?;
    context.run_jobs()?;
    Ok(())
}

0.22.0での出力は次のとおりです。

btoa: Ym9h
function function function
undefined undefined undefined undefined
timer fired
microtask

読み取れることが2つあります。1つ目は、AbortController・TextEncoder・fetch・URL が undefined のままだという点です。boa_wintertc 0.22.0のソースでは、これらのモジュールの register が何もせず Ok(()) を返すだけで、コメントに「Migrate … from boa_runtime」というTODOが残っています(abort/mod.rs)。0.22の時点でこれらが必要なら、従来の boa_runtime を使います。2つ目は実行順で、この環境では setTimeout(…, 0) のコールバックが Promise の then より先に出力されました。ブラウザやNode.jsとは逆の順序なので、タイマーとPromiseの順序に依存するスクリプトを持ち込む場合は、事前に動作を確かめてください。

boa_cliによるREPLとスクリプトの実行

Rustのコードを書く前に挙動だけ見たいなら、CLI版が手早い方法です。cargo install boa_cli で boa コマンドが入り、引数なしで起動するとREPL、ファイルを渡すとそのスクリプトを実行します。

cargo install boa_cli
boa
boa script.js
boa -m main.mjs
boa -e "[1,2,3].map(x => x * 2)"

-m はファイルをES Moduleとして扱うオプションで、import の解決基点は -r で変えられます(既定はカレントディレクトリ)。-e は式を1つ評価して終了し、上の例では [ 2, 4, 6 ] と表示されました。注意したいのは、CLIとライブラリでは使える機能が違う点です。boa_cli 0.22.0 では typeof Intl も typeof fetch も値を返しますが、boa_engine を既定フィーチャで組み込むとどちらも undefined です。CLIで動いたスクリプトが組み込み先で動かないときは、まずフィーチャと登録したランタイムの差を疑ってください。ブラウザで試すだけなら、公式のWASMプレイグラウンドでも同じエンジンが動きます。

wasm32ターゲット向けのビルド設定

wasm32ターゲットでも動きますが、wasm32-unknown-unknown に限り乱数取得のバックエンド指定が必要です。v0.22のREADMEに従い、js フィーチャを有効にしたうえで、プロジェクト直下の .cargo/config.toml に次を書きます。

[target.wasm32-unknown-unknown]
rustflags = '--cfg getrandom_backend="wasm_js"'

WASIやEmscripten向けのターゲットでは、この指定は不要です。

ループ回数・再帰深さの上限設定|Rust側からの強制打ち切り

外部から受け取ったJavaScriptを動かすなら、この機能が採用理由になり得ます。runtime_limits_mut() でループの反復回数と再帰の深さに上限を設定でき、超えた時点で実行を打ち切ります。

use boa_engine::{Context, Source};

fn main() {
    let mut context = Context::default();
    context.runtime_limits_mut().set_loop_iteration_limit(10_000);
    context.runtime_limits_mut().set_recursion_limit(64);

    let result = context.eval(Source::from_bytes("while (true) { }"));
    assert!(result.is_err());
}

0.22.0で確かめた挙動は次の3点です。

  • 無限ループは RuntimeLimitError: reached the maximum number of iteration loops on this execution で止まり、ホスト側のプロセスは落ちない
  • スクリプト側で try { for(;;){} } catch(e) { … } と囲んでも捕まらず、エラーはRust側の eval の戻り値に返る。再帰の上限(reached the maximum number of recursive calls)も同じ
  • 上限に達した後も同じコンテキストで 1+1 を評価すると 2 が返り、コンテキストは使い続けられる

公式の runtime_limits.rs にも「This error cannot be caught in JavaScript, it can only be caught in Rust code.」とあります。スクリプト側が例外を握りつぶして居座る余地がないため、信頼できない入力への防御として機能します。V8を埋め込んで同じことをするには Isolate::TerminateExecution と割り込みの仕組みを自前で組む必要があり、その手間と比べると差は小さくありません。

ただし、この例で設定しているのはループ回数と再帰の深さです。VMのスタックサイズ上限も設定できますが、プロセス全体のメモリ使用量や経過時間を制限するものではありません。巨大な配列を確保するスクリプトは止められないので、別プロセスやOSの資源制限と組み合わせてください。メモリの扱いの基礎はRustのスタックとヒープ|値がヒープへ脱出する条件とスタックオーバーフローの防ぎ方が参考になります。

Boaを選ぶ条件と、選ぶべきでない条件

採用が成立する条件

Boaが向くのは、すでにRustで書かれたアプリケーションに、利用者定義のルールやプラグインをJavaScriptで書かせたい場合です。そのうえで次のどれかが当てはまると、他のエンジンより有利になります。依存にC/C++のビルド環境を持ち込みたくない場合(rquickjs はQuickJS本体をCでビルドするためCコンパイラが要るが、Boaは cargo build だけで通る)、wasm32へそのまま載せたい場合、前章の実行上限のようにホスト側から強制的に止めたい場合です。こうした「Rustアプリにスクリプト実行機能を組み込む」設計から相談したい場合は、フルスクラッチ開発の支援範囲で対応しています。

revm-inspectorsのJSトレーサという採用例

Ethereum実行環境のインスペクタ実装 paradigmxyz/revm-inspectors は、JSトレーサ機能の依存に boa_engine と boa_gc を指定しています。0.22.0公開後の版(2026-10-20公開の0.44.3)ではすでに version = "0.22" に上がっていました。利用者が書いたトレース用スクリプトをRustプロセス内で評価する、Boaの想定どおりの用途です。ただし js-tracer は既定では無効のフィーチャなので、同クレートのダウンロード数をBoaの利用規模とみなすことはできません。

見送るべき条件

最優先で確認するのは、対象処理が要求スループットを満たすかです。掲載ベンチマークではQuickJSの約5分の1のスコアですが、個別処理の差やチューニングの効果までは判断できないので、実際のスクリプトで測ってから決めてください。次の見送り条件は、npmパッケージやNode.js互換APIをそのまま動かしたい場合です。Boaにはファイルシステムもnpmのモジュール解決もないので、その用途ならNode.jsなどのランタイム、速度と成熟度を取るなら rquickjs(0.14.0、直近90日のダウンロード約317万で boa_engine の約1.7倍)が候補になります。仕様の完全準拠が前提になる用途も、fail 480件が残る現状では条件を満たしません。

0.21以前のコードを0.22へ移すときの確認点

日本語の解説は0.18〜0.21時点のものが多く、版ごとの前提が違います。crates.io に記録された各版の要件は次のとおりです。

版 公開日 最低Rustバージョン edition
0.18.0 2024-03-07 1.74.0 2021
0.19.1 2024-09-12 1.79.0 2021
0.20.0 2024-12-05 1.82.0 2021
0.21.1 2026-03-29 1.88.0 2024
0.22.0 2026-08-28 1.91.0 2024

0.21から0.22へは、基本のAPIは変わっていません。v0.21.1とv0.22の examples を比べると、loadstring.rs・closures.rs・runtime_limits.rs はコメントの綴り修正1行を除いて同じで、この記事のコードも書き換えなしで0.22.0でビルドできました。確認が要るのは次の3点です。

  • ツールチェーン:Rust 1.91.0未満ではビルドできない
  • 既定フィーチャ:Temporal が既定で入るようになった。Temporalだけを無効にしたいなら default-features = false, features = ["float16", "xsum"] を指定する。default-features = false だけでは、Float16対応とMath.sumPreciseも無効になる。最小のeval例で default-features = false(既定フィーチャをすべて外した状態)にすると、実行ファイルは13.1MBから12.0MB(11,953,368バイト)に減った
  • Web API:boa_runtime から boa_wintertc への移行が始まった。0.22.0では console・タイマー・マイクロタスク・base64・structuredClone が wintertc 側にあり、fetch・URL・AbortController・TextEncoder はまだ boa_runtime 側

0.20以前からの移行では、0.21で入った非同期まわりの刷新が最も重い変更です。JobQueue が作り直され ModuleLoader が非同期化されたため、独自のジョブキューやモジュールローダーを実装していたコードは書き直しになります。tokioやsmolと組み合わせる例は、公式の examples(tokio_event_loop.rs、smol_event_loop.rs、module_fetch_async.rs)を起点にするのが早道です。

よくある質問

Boa JSとboa_engineは同じものですか?

同じプロジェクトを指します。プロジェクト名がBoa(Boa JSとも呼ばれる)、Rustから使うときのクレート名が boa_engine です。旧クレートの boa は2022年の0.13.1で止まった非推奨品なので、依存には boa_engine を指定してください。

BoaはNode.jsの代わりに使えますか?

使えません。Boaが実装しているのはECMAScript本体で、ファイルシステムやHTTPサーバーといったNode.jsのAPI、npmのパッケージ解決は含まれません。boa_wintertc と boa_runtime で console・タイマー・fetch などを足せますが、それでもRustアプリの中に小さなスクリプト実行環境を作るための部品です。

Boaの最新バージョンはどれですか?

2026年11月時点の最新は boa_engine 0.22.0(2026-08-28公開)です。boajs.dev のブログは0.21の記事が最新のままなので、版の確認はcrates.ioかGitHubのリリース一覧で行ってください。

BoaとQuickJS(rquickjs)はどちらを選ぶべきですか?

速度と成熟度ならQuickJSです。掲載した公式ベンチマークではQuickJSが1,226、Boaが223です。ただし、計測対象はBellard版QuickJSで、rquickjs 0.14.0が使用するQuickJS-NGそのものの比較ではありません。一方で rquickjs はCのビルド環境が必要で、クロスコンパイルやwasm32では純Rustの boa_engine が扱いやすくなります。ビルド環境の制約が厳しいならBoa、性能要件があるならQuickJSという分け方が実務的です。

BoaでES Modules(import)は使えますか?

使えます。Context::default() はカレントディレクトリを基点にファイルを読む標準の SimpleModuleLoader を自動で組み込みます。HTTP取得など独自の読み込み処理が必要な場合は、ModuleLoader を実装します。0.21でローダーが非同期化され、HTTP経由でモジュールを取得する構成も組めるようになりました。実装例は公式の modulehandler.rs と module_fetch_async.rs にあります。試すだけなら boa_cli の -m オプションでファイルをモジュールとして実行できます。

関連記事

お気に入りに入れた記事の一覧

一創の公式 SNS

X では毎朝の新着記事と IT ニュースの速報を、YouTube・TikTok・Instagram・LinkedIn・Facebook では記事の解説動画を配信しています。

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2026.10.29 テックブログ ソニー生命の情報漏洩38.5万件|ログインできない理由、再開状況と窓口の電話番号
  2. 2026.11.03 テックブログ ノジマ不正アクセスで約700万件漏えい:対象確認・窓口と乗っ取り注文を止める実装
  3. 2026.11.02 テックブログ サントリーの不正アクセスで約10万人分漏えい|M365のファイル大量ダウンロードを監査ログで調べる手順
  4. 2026.11.03 テックブログ クラブツーリズム不正アクセス:約432万件の対象確認と偽メールの見分け方・流出検知の設計
  5. 2026.10.31 テックブログ 松屋の不正アクセス:237万件の対象確認と窓口、漏れたパスワードの守り方

RELATED POSTS 関連記事

目次