ARM64_Lab

ネイティブ拡張の npm パッケージを8本入れたら、2本が Visual Studio を要求して落ちた

この記事の見出し
  1. 8本を1本ずつ、空のプロジェクトに入れた
  2. 結果
  3. 入り方が3通りに分かれていた
  4. bcrypt は使わない x64 バイナリも一緒に置いていった
  5. 落ちた2本は、ARM64 だから落ちたのではなかった
  6. 入っただけで終わらせず、動かした
  7. どう判断するか

ネイティブ拡張を持つ 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+1let 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本がビルド経路の入口で止まった、という記録として置いておく。

a
arm64lab — 個人運営

Surface Pro 11th Edition(Snapdragon X Elite)を2025年5月から常用機にしている個人の記録です。ARM64 版 Windows で詰まったところと、その場で測った値をそのまま書き残しています。特定の企業・団体とは関係がなく、いかなる組織を代表する見解でもありません。