Skip to main content

Guide

node-gyp vs prebuild vs napi-rs: Native Addons 2026

Compare node-gyp, prebuild, and @napi-rs/cli for building native Node.js addons. N-API, Rust bindings, pre-built binaries, cross-compilation, and which to use.

·PkgPulse Team·
0

TL;DR

node-gyp is the original build tool for native Node.js addons — compiles C/C++ using GYP (Google's build system) and requires Python + build tools on every install. prebuild generates and distributes pre-compiled binaries so users don't need to compile — it wraps around node-gyp but makes installation painless. @napi-rs/cli is the modern approach — write Rust, compile to N-API binaries with automated cross-compilation and pre-built distribution. In 2026, the best-in-class approach is N-API + prebuild (for C/C++) or @napi-rs (for Rust). Avoid requiring users to compile from source.

Key Takeaways

  • node-gyp: ~40M weekly downloads — the standard C/C++ compiler, requires Python + build tools to install
  • prebuild: ~3M weekly downloads — distributes pre-built binaries, wraps node-gyp
  • @napi-rs/cli: ~500K weekly downloads — Rust-based, N-API, automated cross-compilation and publishing
  • N-API (Node.js API) is ABI-stable — addons built for Node.js 18 work in 20, 22, etc.
  • Users should never need to compile from source — always ship pre-built binaries
  • NEON (Rust) and @napi-rs are the modern path for new native addons

Why Native Addons?

Use native addons when JavaScript isn't fast enough:
- CPU-intensive computation: image processing, video encoding, ML inference
- Accessing OS APIs: system notifications, hardware access, raw sockets
- Integrating existing C/C++ libraries: SQLite, LevelDB, OpenSSL
- Cryptography: hardware-accelerated AES, SHA
- Media processing: sharp (libvips), canvas (Cairo/Skia)

Popular native Node.js packages:
  sharp       — libvips image processing
  canvas      — Cairo rendering
  bcrypt      — password hashing
  sqlite3     — SQLite database
  @napi-rs/canvas — Skia rendering (Rust/N-API)
  duckdb      — DuckDB analytics engine
  leveldown   — LevelDB bindings

node-gyp

node-gyp — the standard C/C++ addon compiler:

binding.gyp (the build file)

{
  "targets": [
    {
      "target_name": "myaddon",
      "sources": ["src/addon.cc"],
      "include_dirs": [
        "<!@(node -p \"require('node-addon-api').include\")"
      ],
      "dependencies": [
        "<!(node -p \"require('node-addon-api').gyp\")"
      ],
      "defines": ["NAPI_DISABLE_CPP_EXCEPTIONS"]
    }
  ]
}

N-API C++ addon (modern, stable ABI)

// src/addon.cc
#include <napi.h>

// Function that does some computation:
Napi::Value CalculateHealthScore(const Napi::CallbackInfo& info) {
  Napi::Env env = info.Env();

  if (info.Length() < 2 || !info[0].IsNumber() || !info[1].IsNumber()) {
    Napi::TypeError::New(env, "Expected two numbers").ThrowAsJavaScriptException();
    return env.Null();
  }

  double downloads = info[0].As<Napi::Number>().DoubleValue();
  double stars = info[1].As<Napi::Number>().DoubleValue();

  // Native computation (fast):
  double score = (downloads * 0.6 + stars * 0.4) / 1000.0;
  double clamped = score > 100 ? 100 : score;

  return Napi::Number::New(env, clamped);
}

Napi::Object Init(Napi::Env env, Napi::Object exports) {
  exports.Set("calculateHealthScore",
    Napi::Function::New(env, CalculateHealthScore));
  return exports;
}

NODE_API_MODULE(myaddon, Init)

Build and use

# Build:
node-gyp configure
node-gyp build
# Or: npm run build (uses node-gyp through package.json scripts)
// Load the native addon:
const addon = require("./build/Release/myaddon.node")

const score = addon.calculateHealthScore(45_000_000, 200_000)
console.log(score)  // Native computation result

The problem node-gyp creates for users

# When users install a package with node-gyp, they see this:
npm install bcrypt

# npm WARN optional Skipping failed optional dependency /chokidar/node_modules/fsevents
# gyp ERR! build error
# gyp ERR! stack Error: not found: make
# gyp ERR! stack     at getNotFoundError (/usr/lib/node_modules/npm/node_modules/which/which.js:14:12)
# gyp ERR! System Linux 5.15.0

# Fix: apt-get install build-essential python3
# This is the user experience problem that prebuild and @napi-rs solve

prebuild

prebuild — distribute pre-built binaries:

How prebuild works

# Package author runs prebuild on CI/CD for each platform:
npx prebuild --runtime node --target 20.0.0  # Node.js 20, current arch
npx prebuild --runtime node --target 22.0.0  # Node.js 22

# Uploads to GitHub Releases as assets:
npx prebuild --upload-all GITHUB_TOKEN

# Creates: prebuilds/linux-x64/myaddon.node, prebuilds/darwin-arm64/myaddon.node, etc.

package.json setup

{
  "scripts": {
    "install": "prebuild-install || node-gyp rebuild",
    "prebuild": "prebuild --strip --verbose"
  },
  "dependencies": {
    "prebuild-install": "^7.0.0",
    "node-gyp": "^9.0.0",
    "node-addon-api": "^7.0.0"
  }
}

User install experience (with prebuild)

# User installs the package:
npm install my-native-package

# prebuild-install downloads the pre-built binary from GitHub Releases
# No compilation needed — fast, no Python, no build tools required!

# Only falls back to compilation if:
# 1. No pre-built binary for their platform/Node.js version
# 2. BUILDING_FROM_SOURCE env var is set

@napi-rs/cli (Rust-based N-API)

@napi-rs — write native addons in Rust:

Why Rust for native addons?

Rust advantages over C/C++:
  - No manual memory management (no use-after-free, buffer overflows)
  - Better error messages and tooling
  - cargo makes dependency management easy
  - async/await support via tokio
  - Cross-compilation is first-class

@napi-rs advantages:
  - TypeScript types generated automatically from Rust function signatures
  - GitHub Actions templates for cross-compilation to all platforms
  - Pre-built binary distribution built-in
  - No binding.gyp files — just Cargo.toml

Create a new @napi-rs project

# Scaffold a new native addon:
npm init @napi-rs my-addon
cd my-addon

# Project structure:
# ├── Cargo.toml       — Rust dependencies
# ├── src/lib.rs       — Rust code
# ├── package.json     — npm package
# ├── index.js         — JS wrapper
# ├── index.d.ts       — TypeScript types (auto-generated)
# └── .github/workflows/CI.yml  — Cross-platform build + publish

Rust implementation

// src/lib.rs
use napi::bindgen_prelude::*;
use napi_derive::napi;

// #[napi] macro generates N-API bindings automatically:
#[napi]
pub fn calculate_health_score(downloads: f64, stars: f64) -> f64 {
  let score = (downloads * 0.6 + stars * 0.4) / 1000.0;
  score.min(100.0)
}

// Async function (runs on Tokio thread pool, non-blocking):
#[napi]
pub async fn process_packages(packages: Vec<String>) -> Result<Vec<f64>> {
  // Expensive computation in Rust thread pool — doesn't block Node.js event loop
  let scores: Vec<f64> = packages.iter()
    .map(|pkg| compute_score(pkg))
    .collect();
  Ok(scores)
}

// TypeScript class:
#[napi]
pub struct PackageAnalyzer {
  threshold: f64,
}

#[napi]
impl PackageAnalyzer {
  #[napi(constructor)]
  pub fn new(threshold: f64) -> Self {
    PackageAnalyzer { threshold }
  }

  #[napi]
  pub fn is_healthy(&self, score: f64) -> bool {
    score >= self.threshold
  }
}

Auto-generated TypeScript types

// index.d.ts — auto-generated from Rust code:

/** Calculate health score from downloads and stars */
export declare function calculateHealthScore(downloads: number, stars: number): number

/** Process packages asynchronously (runs on Rust thread pool) */
export declare function processPackages(packages: Array<string>): Promise<Array<number>>

/** Package analyzer with configurable threshold */
export declare class PackageAnalyzer {
  constructor(threshold: number)
  isHealthy(score: number): boolean
}

Cross-compilation CI

# .github/workflows/CI.yml (generated by @napi-rs scaffold)
name: CI
on: [push, pull_request]

jobs:
  build:
    strategy:
      matrix:
        settings:
          - host: ubuntu-latest
            target: x86_64-unknown-linux-gnu
          - host: ubuntu-latest
            target: aarch64-unknown-linux-gnu
            cross: true
          - host: macos-latest
            target: x86_64-apple-darwin
          - host: macos-latest
            target: aarch64-apple-darwin
          - host: windows-latest
            target: x86_64-pc-windows-msvc

    steps:
      - uses: actions/checkout@v4
      - uses: napi-rs/setup-rust@v1
      - run: yarn build --target ${{ matrix.settings.target }}
      - uses: actions/upload-artifact@v4
        with:
          name: bindings-${{ matrix.settings.target }}
          path: "*.node"

Feature Comparison

Featurenode-gypprebuild@napi-rs
LanguageC/C++C/C++Rust
Pre-built binaries
Cross-compilationManualManual✅ Built-in
TypeScript typesManualManual✅ Auto-generated
ABI stability (N-API)✅ if using N-API
User compile required
Memory safetyManualManual✅ Rust
Async/awaitComplexComplex✅ Native
Ecosystem maturity✅ Battle-testedGrowing fast
Setup complexityLowMediumLow (scaffold)

When to Use Each

Choose node-gyp if:

  • Maintaining an existing C/C++ addon
  • Wrapping an existing C library with no Rust FFI available
  • The addon will always be compiled locally (internal tool)

Choose prebuild if:

  • Publishing a C/C++ addon that users will install
  • You want fast installs with no compilation for end users
  • Maintaining existing node-gyp codebase but need better distribution

Choose @napi-rs if:

  • Writing a new native addon from scratch in 2026
  • Want memory-safe native code without C/C++ footguns
  • Need async operations that don't block the Node.js event loop
  • Want TypeScript types auto-generated from Rust code

Community Adoption in 2026

node-gyp reaches approximately 50 million weekly downloads, but this number is heavily misleading — node-gyp is installed as a dependency by npm itself (to handle packages that need compilation as a fallback). Most of these downloads are not from projects that actively use node-gyp for addon development. Actual C/C++ addon authors using node-gyp are a small subset. Node-gyp's pervasive presence in node_modules reflects npm's bundling strategy, not intentional adoption.

prebuild at approximately 500,000 weekly downloads represents the actual C/C++ addon distribution ecosystem. Packages like sharp, better-sqlite3, canvas, and bcrypt use prebuild or its successor prebuildify to distribute platform-specific binaries on GitHub Releases. Users installing these packages rarely invoke the compiler — prebuild-install fetches the appropriate binary automatically. For C/C++ addon authors, adding prebuild distribution is essentially table stakes for a user-friendly package.

@napi-rs has grown rapidly to approximately 300,000 weekly downloads for the CLI and tooling, but the actual reach is better measured by the packages built with it: swc (the JavaScript compiler), rolldown (Rust-based bundler), lightningcss, farm, and dozens of other performance-critical tools in the JavaScript ecosystem are built with napi-rs. These packages collectively reach hundreds of millions of end-user installs. For new native addon development in 2026, napi-rs is the unambiguous choice for Rust authors, and its memory safety and async capabilities make it compelling even for teams previously working in C/C++.

Cross-Platform Build Strategy and CI/CD Considerations

Building native Node.js addons that reliably install across Windows, macOS (x64 and arm64), and Linux (glibc and musl) requires significant CI infrastructure investment.

node-gyp on CI requires installing build tools on every CI runner: python3, make, and platform-specific compilers (gcc on Linux, Visual Studio Build Tools on Windows, Xcode Command Line Tools on macOS). This adds 1-3 minutes to CI run times for fresh environments. Docker-based CI (GitHub Actions with ubuntu-latest) can use apt-get install build-essential reliably, but Windows CI runners require careful setup of Visual Studio Build Tools. Apple Silicon (arm64) became a common macOS target starting in 2022, requiring either cross-compilation or separate arm64 CI runners.

prebuild's precompiled binary strategy moves the compilation cost from install time to publish time. The package author runs prebuild --runtime node --target 22.0.0 --arch x64,arm64 --platform linux,darwin,win32 (or similar) and uploads the resulting .tar.gz binaries as GitHub Release assets. Users install the package, prebuild-install downloads the matching binary, and no local compiler is needed. The CI infrastructure required is a matrix of OS/arch combinations — typically 6-8 runners for full coverage. This is significant setup work but dramatically improves the install experience for users.

napi-rs's prebuilt approach is similar to prebuild but better integrated into the Rust toolchain. The napi-rs/napi-rs GitHub Actions CI template handles the matrix build automatically, using cross for Linux cross-compilation and native arm64 macOS runners for Apple Silicon. The template produces GitHub Release assets that npm install downloads automatically via the platform-specific optional dependencies pattern ("optionalDependencies": {"my-pkg-darwin-x64": "^1.0.0", "my-pkg-linux-arm64": "^1.0.0"}). This is the recommended distribution mechanism for new native addons in 2026 — it avoids the need for build tools on end users' machines while keeping installation fast and reliable.

For packages targeting serverless environments (Lambda, Vercel Functions, Cloudflare Workers), native addons introduce deployment complexity because precompiled binaries must match the deployment target's OS/arch/glibc version exactly. napi-rs packages targeting Lambda typically provide a linux-x64-gnu variant compiled against Amazon Linux 2's glibc version.

A practical deployment consideration is that container images for Node.js applications with native addons must match the prebuilt binary's target architecture. If your CI builds a Docker image on an x64 runner but deploys to an ARM-based Kubernetes cluster (e.g., AWS Graviton or Apple Silicon on Apple Cloud), the prebuilt binary will fail with an architecture mismatch error at startup. Multi-arch Docker builds (using docker buildx) and matching prebuilt binaries for the target architecture resolve this. For serverless deployments on AWS Lambda, specifying the exact Amazon Linux version in your prebuild or napi-rs build matrix ensures binary compatibility with the Lambda execution environment.

Methodology

Download data from npm registry (weekly average, February 2026). Feature comparison based on node-gyp v9.x, prebuild v12.x, and @napi-rs/cli v2.x.

Compare build tooling and native packages on PkgPulse →

The N-API (Node-API) stability guarantee deserves emphasis for teams evaluating the long-term maintenance burden of native addons. Before N-API, native addons were compiled against a specific V8 engine version, meaning every major Node.js release required recompilation. A package using N-API and publishing pre-built binaries for Node.js 18 will have those exact same binaries work in Node.js 20, 22, and beyond — no recompilation, no new CI runs for Node.js version upgrades. This ABI stability is the single biggest operational improvement in the native addon ecosystem over the past five years. All three approaches (node-gyp with N-API, prebuild, and @napi-rs) benefit from this guarantee as long as the addon uses the N-API surface rather than V8 internals directly.

One practical gotcha with napi-rs that is not obvious from the documentation: the auto-generated index.js wrapper uses conditional requires to load the platform-appropriate .node binary. In monorepo setups where the binary package is symlinked via workspace protocols (pnpm workspaces, Yarn workspaces), the path resolution from index.js to the binary can fail if the symlink resolves to a different node_modules directory than expected. The standard fix is to publish the platform binaries as separate optional dependencies (my-pkg-darwin-arm64, my-pkg-linux-x64) and let the package manager resolve the correct one for the current platform — this is the pattern used by SWC, Rolldown, and most production napi-rs packages. Testing your binary resolution in both hoisted and non-hoisted node_modules configurations before publishing saves significant debugging time downstream.

See also: cac vs meow vs arg 2026 and cosmiconfig vs lilconfig vs conf, archiver vs adm-zip vs JSZip (2026).

The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.