Installing eight native npm packages on ARM64 Windows: two required Visual Studio
In this article
I installed eight npm packages with native extensions on ARM64 Windows. Six installed in roughly 10 to 22 seconds, while two failed after asking for Visual Studio.
On the Python side, I measured the availability of win_arm64 wheels for 20 major packages on 2026-08-02, where wheel tags usually made installation availability readable. Node.js has no equivalent wheel-tag display. Even when npm install appears successful, I could not tell which architecture the installed files targeted without opening them. I therefore read the PE headers of the .node and .dll files after installation.
The measurement date was August 5, 2026. The machine was a Surface Pro 11th Edition with a Snapdragon X Elite X1E80100 and Windows_NT 10.0.26200. Node.js was the v24.13.0 arm64 build, and npm was 11.6.2.
Install eight packages one at a time in empty projects
For each package, I created an empty directory and an empty package.json, and assigned a separate npm cache location. Installing them all in one project would share dependencies and make it unclear which package brought in what. Separate caches also avoided making only the second package onward look fast. Every package was tested as a first-time install.
python scripts/npm_native_prebuild_census.py
All versions were pinned. Leaving them floating would make it impossible to return to the same result later. The packages were sharp 0.34.4 for image processing, better-sqlite3 12.4.1 for an embedded database, bcrypt 6.0.0 for password hashing, bufferutil 4.0.9 for WebSockets, @swc/core 1.13.5 for the TypeScript compiler, esbuild 0.25.10 for bundling, lmdb 3.4.2 for a key-value store, and canvas 3.2.0 for rendering.
Results
| Package | Elapsed | rc | Installed artifact | machine |
|---|---|---|---|---|
| sharp 0.34.4 | 14.15 seconds | 0 | 3 files from @img/sharp-win32-arm64 | ARM64 |
| better-sqlite3 12.4.1 | 21.48 seconds | 0 | build/Release/better_sqlite3.node | ARM64 |
| bcrypt 6.0.0 | 7.85 seconds | 0 | prebuilds/win32-arm64/bcrypt.node | ARM64 |
| bufferutil 4.0.9 | 10.89 seconds | 1 | none | — |
| @swc/core 1.13.5 | 16.31 seconds | 0 | swc.win32-arm64-msvc.node | ARM64 |
| esbuild 0.25.10 | 11.12 seconds | 0 | @esbuild/win32-arm64/esbuild.exe | ARM64 |
| lmdb 3.4.2 | 15.37 seconds | 0 | @lmdb/lmdb-win32-arm64/node.napi.node | ARM64 |
| canvas 3.2.0 | 45.52 seconds | 1 | none | — |
All six successful installs were ARM64 according to the PE headers. I did not get the unsettling result where an x64 binary is silently placed as the main artifact and runs anyway. That check mattered because emulation would change the interpretation of every performance discussion.
Three different installation paths
The fact that a package “installed” did not mean all six used the same mechanism. I found three patterns.
The most common pattern was a platform-specific package supplied as an optionalDependency, with npm choosing the appropriate one. sharp fetched @img/sharp-win32-arm64, @swc/core fetched @swc/core-win32-arm64-msvc, lmdb fetched @lmdb/lmdb-win32-arm64, and esbuild fetched @esbuild/win32-arm64. The logs for these four contain no node-gyp text. No build step ran; installation ended after downloading the one required package.
The files left by sharp make this easy to see: libvips-42.dll was 15216640 bytes, libvips-cpp-8.17.2.dll was 440832 bytes, and sharp-win32-arm64.node was 435712 bytes. All were ARM64. The C++ libraries were available as ARM64 builds too.
The second pattern was bundling every platform's files in the package itself, which is what bcrypt did. The third was better-sqlite3, where prebuild-install retrieves a prebuilt binary during installation. Its log includes node-gyp, but the operation ended in 21.48 seconds; that was not the time to compile SQLite. A prebuilt binary was found, so it did not proceed further.
bcrypt also left an unused x64 binary
The surprising package was bcrypt: its prebuilds directory contained seven platform variants.
| Directory | Size |
|---|---|
| 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 |
Of the total 968.7KB, this machine uses only the 178.5KB win32-arm64 file. The remaining 790.2KB is left in place and untouched. The PE header confirms that the 191KB win32-x64 file is actually x64, so an x64 binary sits inside the ARM64 installation. It is not harmful because it is not executed, but when I first saw the file list I wondered whether the wrong architecture had been selected. You cannot decide that until you verify which file is loaded.
The two failures were not simply “because ARM64”
At first, I suspected that bufferutil and canvas failed because ARM64 was unsupported. The logs showed something more specific: both stopped at the same point.
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
No prebuilt binary was found, so installation moved toward a node-gyp source build and ended while searching for Visual Studio. The C++ compilation itself never started. For these two packages, the measurement establishes only that a binary ready to install on Node.js 24 for ARM64 Windows was not available. It does not establish that they cannot build on ARM64. The result could change on a machine with the Desktop development with C++ workload installed. These are separate claims.
It was also inconvenient that canvas waited 45.52 seconds before failing. prebuild-install searches for candidates before falling through to node-gyp. The experience was similar to orjson taking 113.28 seconds before failing on 2026-07-11. If the tool simply said “no compatible binary,” it would be easier to decide what to do.
Do not stop at installation; run the packages
Even an npm install return code of 0 leaves open the possibility that loading the package will fail. I therefore required all six successful packages and gave each one a small task.
python scripts/npm_native_runtime_check.py
sharp created a 64×32 image and converted it to PNG, returning 176 bytes. The libvips version was 8.17.2. better-sqlite3 created an in-memory table, inserted one value, and responded as SQLite 3.50.4. bcrypt generated and verified a 60-character hash. @swc/core converted TypeScript to 29-character JavaScript, and esbuild reduced let x=1;let y=x+1 to let x=1,y=x+1;. lmdb read back the value it had written. All six worked.
I also captured the loaded .node path. bcrypt loaded prebuilds/win32-arm64/bcrypt.node, not the bundled win32-x64 file. The mix-up I was concerned about did not occur.
How I interpret this
My local conclusion is that using Node.js native extensions on ARM64 Windows depends less on an abstract “ARM64 supported” label than on whether a prebuilt binary exists. If one exists, installation finishes in the 10-to-20-second range and the package runs. If not, the task changes into a node-gyp source build.
I now look once at node_modules before adding a new dependency. A platform-specific package is a reassuring sign; a .node under build/Release indicates that the build path was involved. That is enough to make a fairly good initial judgment.
I have not decided whether to install Visual Studio's C++ workload. It might allow bufferutil and canvas to succeed, but keeping several gigabytes of development tools around for these two packages alone is not especially compelling. For now, I am first looking for packages with the same role.
If I measure this again, I want to install the C++ workload and see whether bufferutil can actually build. That belongs in a separate article. This one records that six of eight packages installed with distributed ARM64 binaries, while two stopped at the entrance to the build path.