はじめに
Node.jsで重たい計算処理を書いていて、「ここだけネイティブコードで書けたら速いのに」と思ったことはないですか。
画像変換、暗号処理、大量データのパース。JavaScriptでも書けるけれど、CPUバウンドな処理はどうしても限界があります。かといって、C++でネイティブアドオンを書くのはnode-gypとの格闘が待っていて、正直気が重い。ビルド環境の構築だけで半日溶けた経験がある方も多いのではないでしょうか。
そんな悩みを解決してくれるのがNAPI-RSです。Rustで書いた関数に属性マクロをひとつ付けるだけで、Node.jsから呼び出せるネイティブアドオンが完成します。しかもTypeScriptの型定義まで自動生成してくれるのです。
実際、SWC、Prisma、Polars、Next.jsといった名だたるプロジェクトがNAPI-RSを採用しています。あの圧倒的なビルド速度の裏側には、このフレームワークがいたわけですね。
NAPI-RSとは
NAPI-RSは、Node-APIを介してRustでNode.jsのネイティブアドオンを構築するためのフレームワークです。「Building pre-compiled Node.js addons in Rust」というスローガンの通り、事前コンパイル済みのバイナリを配布する仕組みまで含めてサポートしてくれます。
2026年7月時点の最新版はv3系で、現在も活発にリリースが続いています。Node-APIのABI安定性のおかげで、一度ビルドしたバイナリが複数のNode.jsバージョンでそのまま動くのも大きな魅力です。
主な特徴
- node-gyp不要のシンプルなビルド -
napi buildコマンドひとつでビルドが完結します。C++アドオン開発で悩まされたPythonやビルドツールチェーンへの依存から解放されます - TypeScript型定義の自動生成 - Rustの関数シグネチャから
.d.tsファイルを自動生成。Rust側の型安全性がそのままJavaScript側にも届きます - 圧倒的なクロスプラットフォーム対応 - Windows、macOS、Linux(glibc/musl)、FreeBSD、Androidに加え、WebAssembly(WASI)へのフォールバックまでサポートします
- 非同期処理のフルサポート - Rustのasync関数がJavaScriptのPromiseにそのままマッピングされ、イベントループをブロックしません
- 豊富な型変換 - 数値・文字列はもちろん、Buffer、TypedArray、BigInt、Promise、さらにはserdeを使った構造体の変換まで対応しています
インストール
NAPI-RSにはプロジェクトの雛形を生成するCLIが用意されています。まずはこれを使って新しいプロジェクトを作成しましょう。
# npm
npm create @napi-rs/cli@latest
# yarn
yarn create @napi-rs/cli
# pnpm
pnpm create @napi-rs/cli
対話形式でプロジェクト名やターゲットプラットフォームを聞かれるので、順に答えていくだけで、Rust側のCargo.toml、src/lib.rs、そしてnpmパッケージとして公開するための設定一式が生成されます。
なお、事前にRustのツールチェーンが必要です。未インストールの場合はrustupからインストールしておいてください。Rust 1.88.0以降が必要になります。
# Rustのバージョン確認
rustc --version
基本的な使い方
生成されたプロジェクトのsrc/lib.rsを見てみましょう。NAPI-RSの魔法は#[napi]属性マクロにあります。
use napi_derive::napi;
#[napi]
pub fn fibonacci(n: u32) -> u32 {
match n {
1 | 2 => 1,
_ => fibonacci(n - 1) + fibonacci(n - 2),
}
}
たったこれだけです。#[napi]を付けた関数が、そのままNode.jsから呼び出せる関数になります。ビルドしてみましょう。
npm run build
すると、プラットフォームごとの.nodeバイナリと、TypeScript型定義が自動生成されます。
// index.d.ts(自動生成)
export function fibonacci(n: number): number
JavaScript側からは、普通のnpmパッケージと同じ感覚で使えます。
import { fibonacci } from './index.js'
console.log(fibonacci(30)) // 832040
Rust側でu32と宣言した引数は、JavaScript側ではnumberとして型チェックされます。手書きのバインディングコードは一行もありません。この体験を一度味わうと、もうnode-gypには戻れないですね。
非同期関数もそのままPromiseに
CPUバウンドな処理でイベントループをブロックしたくない場合は、async関数にするだけです。
use napi_derive::napi;
#[napi]
pub async fn heavy_computation(input: u32) -> u32 {
// 別スレッドで実行され、イベントループをブロックしない
tokio::task::spawn_blocking(move || {
// 重たい計算処理
(1..=input).sum()
})
.await
.unwrap()
}
JavaScript側では、ごく自然にawaitで受け取れます。
const result = await heavyComputation(1000000)
実践的なユースケース
画像処理をRustに任せる
実務でよくあるのが、アップロードされた画像のリサイズやフォーマット変換です。クラスもそのままエクスポートできるので、状態を持つAPIも自然に書けます。
use napi::bindgen_prelude::*;
use napi_derive::napi;
#[napi]
pub struct ImageProcessor {
data: Vec<u8>,
}
#[napi]
impl ImageProcessor {
#[napi(constructor)]
pub fn new(data: Uint8Array) -> Self {
Self { data: data.to_vec() }
}
#[napi]
pub fn byte_length(&self) -> u32 {
self.data.len() as u32
}
}
JavaScript側からはクラスとしてそのまま使えます。
import { readFile } from 'node:fs/promises'
import { ImageProcessor } from './index.js'
const buffer = await readFile('./photo.jpg')
const processor = new ImageProcessor(buffer)
console.log(`サイズ: ${processor.byteLength()} bytes`)
実際にこのパターンで作られているのが@napi-rs/imageパッケージで、WebP変換などが数行で書けるようになっています。
import { Transformer } from '@napi-rs/image'
const transformer = new Transformer(imageBytes)
const webp = await transformer.toWebp()
構造体でリッチなデータをやり取りする
Rustの構造体に#[napi(object)]を付けると、JavaScriptのプレーンオブジェクトと相互変換できます。
use napi_derive::napi;
#[napi(object)]
pub struct AnalyzeResult {
pub word_count: u32,
pub line_count: u32,
pub longest_word: String,
}
#[napi]
pub fn analyze_text(text: String) -> AnalyzeResult {
let words: Vec<&str> = text.split_whitespace().collect();
AnalyzeResult {
word_count: words.len() as u32,
line_count: text.lines().count() as u32,
longest_word: words
.iter()
.max_by_key(|w| w.len())
.unwrap_or(&"")
.to_string(),
}
}
const result = analyzeText('NAPI-RS makes native addons easy')
console.log(result.wordCount) // 5
console.log(result.longestWord) // "native"
注目してほしいのは、Rust側のsnake_caseがJavaScript側では自動的にcamelCaseに変換されている点です。両言語の流儀を保ったまま開発できる、細やかな配慮が効いています。
CI/CDでのクロスコンパイルと配布
NAPI-RSの真骨頂は配布の仕組みにあります。CLIが生成するGitHub Actionsのワークフローを使うと、各プラットフォーム向けのバイナリをビルドし、@your-pkg/core-darwin-arm64のようなプラットフォーム別npmパッケージとして公開できます。
利用者はoptionalDependenciesの仕組みによって自分の環境に合ったバイナリだけをダウンロードするので、インストール時のコンパイルは一切発生しません。「npm installしたらnode-gypのエラーで動かない」という悪夢とは無縁です。
まとめ
NAPI-RSを使えば、Rustの安全性とパフォーマンスをNode.jsプロジェクトにシームレスに持ち込めます。ポイントを振り返りましょう。
#[napi]マクロを付けるだけでRust関数がNode.jsから呼び出せる- TypeScript型定義が自動生成され、型安全性が両言語をまたいで保たれる
- async関数はPromiseに自動マッピングされ、イベントループをブロックしない
- 事前コンパイル済みバイナリの配布までフレームワークがサポートしてくれる
SWCやPrismaといった実績あるプロジェクトが支えるエコシステムなので、プロダクション投入も安心です。まずはボトルネックになっているCPUバウンドな処理をひとつ選んで、Rustに置き換えてみてはいかがでしょうか。JavaScriptとRust、両方のいいとこ取りができる開発体験を、ぜひ味わってみてください。
