ネイティブ拡張の npm パッケージを8本入れたら、2本が Visual Studio を要求して落ちた
この記事の見出し
ネイティブ拡張を持つ npm パッケージを8本、ARM64 Windows に入れてみたところ、6本は10秒から22秒ほどで入り、2本が Visual Studio を要求して落ちた。
Python 側では2026-08-02 に主要20パッケージの win_arm64 wheel の有無を測っていて、そこでは wheel タグを見れば入るかどうかがだいたい読めた。Node.js には wheel タグに当たる表示が無い。npm install が成功したように見えても、置かれた実体がどのアーキテクチャ向けなのかは、ファイルを開いてみるまで手元では分からなかった。そこで、入れたあとに .node と .dll の PE ヘッダーまで読むことにした。
計測日は2026-08-05。機材は Surface Pro 11th Edition の Snapdragon X Elite X1E80100、Windows_NT 10.0.26200 で、Node.js は v24.13.0 の arm64 ビルド、npm は 11.6.2 だ。
8本を1本ずつ、空のプロジェクトに入れた
パッケージごとに空のディレクトリと空の package.json を作り、npm キャッシュもパッケージごとに別の場所を割り当てた。まとめて1つのプロジェクトに入れると、依存が共有されてどれが何を連れてきたのか分からなくなる。キャッシュを分けたのは、2本目以降だけ速く見える状態を避けたかったからだ。どれも「初めて入れる」条件で揃えている。
python scripts/npm_native_prebuild_census.py
バージョンは全部固定した。ここを浮かせると、あとから同じ結果に戻れなくなる。選んだのは画像処理の sharp 0.34.4、組み込み DB の better-sqlite3 12.4.1、パスワードハッシュの bcrypt 6.0.0、WebSocket 用の bufferutil 4.0.9、TypeScript コンパイラの @swc/core 1.13.5、バンドラの esbuild 0.25.10、キーバリューストアの lmdb 3.4.2、そして描画の canvas 3.2.0 だ。
結果
| パッケージ | 所要 | rc | 置かれた実体 | machine |
|---|---|---|---|---|
| sharp 0.34.4 | 14.15 秒 | 0 | @img/sharp-win32-arm64 の3ファイル | ARM64 |
| better-sqlite3 12.4.1 | 21.48 秒 | 0 | build/Release/better_sqlite3.node | ARM64 |
| bcrypt 6.0.0 | 7.85 秒 | 0 | prebuilds/win32-arm64/bcrypt.node | ARM64 |
| bufferutil 4.0.9 | 10.89 秒 | 1 | 無し | — |
| @swc/core 1.13.5 | 16.31 秒 | 0 | swc.win32-arm64-msvc.node | ARM64 |
| esbuild 0.25.10 | 11.12 秒 | 0 | @esbuild/win32-arm64/esbuild.exe | ARM64 |
| lmdb 3.4.2 | 15.37 秒 | 0 | @lmdb/lmdb-win32-arm64/node.napi.node | ARM64 |
| canvas 3.2.0 | 45.52 秒 | 1 | 無し | — |
入った6本は、PE ヘッダーを読んだ限りどれも ARM64 だった。x64 のバイナリが本体として置かれて黙って動いている、という気味の悪い結果にはならなかった。エミュレーションで動いていたら性能の話が全部ずれるので、ここは確認しておきたかった部分だ。
入り方が3通りに分かれていた
同じ「入った」でも、中身の仕組みは揃っていない。数えると3通りあった。
いちばん多かったのが、プラットフォーム別のパッケージを optionalDependencies として持ち、npm に選ばせる形だ。sharp は @img/sharp-win32-arm64 を、@swc/core は @swc/core-win32-arm64-msvc を、lmdb は @lmdb/lmdb-win32-arm64 を、esbuild は @esbuild/win32-arm64 を引いてきた。この4本のログには node-gyp の文字が出てこない。ビルドという工程がそもそも走らず、必要な1つを落としてくるだけで終わる。
sharp が置いていったものが分かりやすい。libvips-42.dll が15216640バイト、libvips-cpp-8.17.2.dll が440832バイト、sharp-win32-arm64.node が435712バイトで、いずれも ARM64 だった。C++ のライブラリごと ARM64 版が用意されている。
2つめが、全プラットフォーム分をパッケージ本体に同梱しておく形で、bcrypt がこれだった。3つめが better-sqlite3 で、こちらはインストール時に prebuild-install がビルド済みバイナリを取りに行く。ログには node-gyp の文字も出るが、21.48秒で終わっていて、SQLite を実際にコンパイルした時間ではない。ビルド済みが見つかったので、その先へ進まずに済んでいる。
bcrypt は使わない x64 バイナリも一緒に置いていった
意外だったのは bcrypt で、prebuilds の下に7つのプラットフォーム分が並んでいた。
| ディレクトリ | サイズ |
|---|---|
| win32-arm64 | 178.5 KB |
| win32-x64 | 191 KB |
| linux-x64 | 177.1 KB |
| linux-arm64 | 149.6 KB |
| linux-arm | 131.8 KB |
| darwin-arm64 | 86.5 KB |
| darwin-x64 | 54.2 KB |
合計968.7KB のうち、この機械で使うのは win32-arm64 の178.5KB だけだ。残りの790.2KB は置かれたまま触られない。PE ヘッダーを読むと win32-x64 の191KB はきちんと x64 で、ARM64 機に x64 のバイナリが1つ転がっている状態になる。実行されないので害は無いものの、最初にファイル一覧を見たときは、間違ったアーキテクチャを掴まされたのかと詰まった。読み込まれるのがどれかを確かめるまでは判断できない。
落ちた2本は、ARM64 だから落ちたのではなかった
bufferutil と canvas の失敗は、正直なところ最初は ARM64 非対応を疑った。ログを読むと違っていて、2本とも同じところで止まっている。
gyp ERR! find VS You need to install the latest version of Visual Studio
gyp ERR! find VS including the "Desktop development with C++" workload.
gyp ERR! configure error
gyp ERR! stack Error: Could not find any Visual Studio installation to use
gyp ERR! node -v v24.13.0
gyp ERR! node-gyp -v v11.4.2
配布済みバイナリが見つからず node-gyp のソースビルドへ進み、その手前の Visual Studio 探索で終わっている。C++ のコンパイル自体が始まってすらいない。つまりこの2本について測れたのは「Node.js 24 の ARM64 Windows 向けに、そのまま入るバイナリが用意されていない」ことまでで、ARM64 でビルドできないかどうかは分かっていない。Desktop development with C++ を入れた機械なら結果が変わる可能性は残る。ここを混ぜて書くと嘘になるので、分けておく。
canvas が45.52秒かけてから落ちるのも面倒だった。prebuild-install が候補を探しに行ってから node-gyp に落ちるぶん、待たされる。2026-07-11 に orjson が113.28秒待たせてから失敗したときと、体感はよく似ている。すぐ「対応するバイナリがありません」と言ってくれれば、こちらの判断も早い。
入っただけで終わらせず、動かした
npm install の戻り値が0でも、読み込んだ瞬間に落ちる可能性は残る。そこで6本を実際に require して、1回ずつ仕事をさせた。
python scripts/npm_native_runtime_check.py
sharp は64×32の画像を作って PNG に変換し、176バイトを返した。libvips のバージョンは 8.17.2 と出ている。better-sqlite3 はメモリ上にテーブルを作って値を1つ入れ、SQLite 3.50.4 として応答している。bcrypt は60文字のハッシュを作って照合まで通った。@swc/core は TypeScript を29文字の JavaScript に変換し、esbuild は let x=1;let y=x+1 を let x=1,y=x+1; に縮めた。lmdb は書いた値をそのまま読み出せている。6本とも動いた。
読み込まれた .node の実体も取った。bcrypt が掴んだのは prebuilds/win32-arm64/bcrypt.node で、同梱されている win32-x64 のほうではない。心配していた取り違えは起きていなかった。
どう判断するか
手元の結論としては、ARM64 Windows で Node.js のネイティブ拡張を使うかどうかは、ARM64 対応の有無ではなく「配布済みバイナリが用意されているか」で決まる。用意されていれば10秒台から20秒台で入り、そのまま動く。無ければ node-gyp のソースビルドという別の作業に移る。
個人的には、新しい依存を足す前に node_modules の下を1回覗くようにした。プラットフォーム別パッケージが引かれていれば安全側で、build/Release の下に .node ができていればビルド経路を通っている。判断はそれだけでかなりつく。
Visual Studio の C++ ワークロードを入れるかどうかは、まだ決めていない。入れれば bufferutil と canvas が通るかもしれない一方で、この2本のために数GBの開発環境を常駐させる理由も薄い。今のところは、同じ役割を持つ別のパッケージを探すほうを先に試している。
次に測るなら、C++ ワークロードを入れた状態で bufferutil が本当にビルドできるのかを見たい。それは別の記事に分ける。今回は、8本のうち6本が配布済み ARM64 バイナリで入り、2本がビルド経路の入口で止まった、という記録として置いておく。