Node.js Internals
Native Addons: C++ Extensions
When Slack rewrote a critical section of code from JavaScript to C++, message processing latency dropped from 300ms to 50ms. When Discord integrated a Rust library via a native addon, the throughput of voice encoding increased tenfold. Native addons are a way to go beyond the capabilities of pure JavaScript, providing direct access to hardware, existing C/C++ libraries, and system APIs. They are a last resort for performance-critical tasks, but when needed, the speed difference is measured in orders of magnitude.
- **bcrypt** - every project with authentication uses a native addon for password hashing. A pure-JS implementation would be 100 times slower, making bcrypt useless for protection against brute-force attacks.
- **sharp** - image processing on servers (resize, crop, optimize). Used in Medium, Unsplash, 500px for generating thumbnails. Processes 1000 images/second thanks to libvips (a C++ library with SIMD optimizations).
- **sqlite3** - an embedded database for local data, logs, cache. It is used in Electron applications (VS Code, Slack, Discord) for storing settings and history. Native bindings allow using production-grade SQLite directly in Node.js.
- **serialport** - control of Arduino, Raspberry Pi, USB devices from Node.js. Used in IoT projects, home automation, robotics. Without a native addon, access to serial ports is impossible from JavaScript.
Intro
Imagine you are building a race car. JavaScript is the comfortable cabin with autopilot (garbage collection, event loop, high-level APIs). But sometimes you need to open the hood and manually tune the engine - get direct access to the hardware, system libraries, or existing C/C++ code. **Native Addons** are the bridge between the world of JavaScript and the world of native code.
**Native Addon** is a dynamic library (`.node` file, actually `.so`/`.dylib`/`.dll`) written in C/C++ that can be connected to Node.js via `require()` like a regular module. Inside, the addon has direct access to the V8 API, libuv, OpenSSL, and everything available to a C++ program. This allows the use of existing C/C++ libraries (image processing, cryptography, machine learning), writing performance-critical code sections, or integrating with system APIs.
**Real examples of native addons in production:** `bcrypt` (password hashing), `node-sass` (SASS compilation via libsass), `sharp` (image processing via libvips), `sqlite3` (bindings to SQLite), `robotjs` (mouse/keyboard control), `canvas` (graphics rendering via Cairo). Without native addons, these libraries would be tens of times slower or even impossible in Node.js.
When a native addon is needed
**NEEDED:** - Password hashing (bcrypt) - CPU-intensive, blocks the Event Loop - Image processing (sharp) - requires libvips performance - Machine learning (tensorflow.js native) - needs CUDA/AVX instructions - Integration with the company's legacy C++ code - System calls unavailable from JS (low-level network, USB) **NOT NEEDED:** - Simple calculations - Worker Threads are sufficient - I/O operations - async Node.js API is already efficient - JSON parsing - built-in parser optimized by V8 - Business logic - complexity of development/maintenance is not worth it **Rule:** Native addon is a last resort when profiling has shown a real bottleneck, and Worker Threads do not solve the problem.
**Dangers of native addons:** 1. **Segmentation fault = crash of the entire Node.js process**. In JavaScript, an error in the code = exception. In C++, an error = undefined behavior, which can kill the server. 2. **Memory leaks are not detected by V8 GC**. If you allocated memory using `malloc()` and did not free it - the leak is permanent. 3. **Blocking the Event Loop**. If a C++ function runs synchronously for 1 second - the Event Loop is blocked. Async workers are needed. 4. **ABI compatibility**. A compiled addon is tied to the Node.js/V8 version. Updating Node.js = recompiling all addons. 5. **Complexity of development**. C++ requires understanding pointers, memory management, multithreading. One mistake = production outage.
You have an API server on Node.js. One of the operations - image compression using the pure-JS library `pngquant-js` - takes 800ms and blocks the Event Loop. You are considering a native addon (sharp/libvips). What risks should be considered?
N-API
**N-API (Node-API)** is a stable C API for creating native addons, ensuring ABI (Application Binary Interface) compatibility between Node.js versions. This addresses the main issue of older addons: previously, when updating Node.js/V8, all addons had to be rebuilt because the internal V8 API was constantly changing. N-API is a layer that isolates the addon from V8 changes.
Imagine a USB connector. Before N-API, each version of Node.js had its own unique connector (V8 API). Updated Node.js - all devices (addons) stopped working, requiring recompilation. N-API is a standardized USB-C: compile an addon once, and it works on Node.js 10, 12, 14, 16, 18, 20... without recompilation. This is a **revolution** for the native addons ecosystem.
**N-API has become the standard:** Since Node.js 10, N-API is stable and recommended for all new addons. Popular libraries have migrated: `bcrypt`, `sqlite3`, `sharp`. Advantages: ABI stability, forward compatibility, easier maintenance (no need to track V8 changes), fewer dependencies in CI/CD.
Real Case: Migrating bcrypt to N-API
`bcrypt` - a popular library for password hashing (used in 90% of Node.js projects with authentication). Until version 3.0, it used the old V8 API: **Problem:** - With the release of Node.js 10, 12, 14, new versions of bcrypt needed to be published - Builds would fail on production servers when updating Node.js (missing prebuilt binaries) - The support team spent weeks rebuilding for new platforms **Solution - migration to N-API:** - Rewrote bindings from V8 API to N-API - A binary compiled once works on Node.js 10-20+ - CI was simplified: prebuilt binaries for Node.js 10 work everywhere - The number of issues on GitHub decreased by 70% **Result:** bcrypt@5.x supports N-API, installs without problems on any version of Node.js. This has become the standard for all popular native addons.
**N-API vs old V8 API:** **N-API (recommended):** - ✅ ABI stable - works on different versions of Node.js - ✅ Forward compatible - an addon for Node.js 12 works on 18 - ✅ Easier maintenance - no need to track V8 changes - ❌ Slightly more boilerplate code - ❌ No access to low-level V8 API (isolate, context) **V8 API (legacy):** - ✅ Direct access to all V8 capabilities - ✅ Slightly less overhead (but the difference is negligible) - ❌ Breaks when updating Node.js/V8 - ❌ Needs to be rebuilt for each Node.js version - ❌ Deprecated for new projects **Verdict:** For new projects ALWAYS use N-API. For legacy projects - migration to N-API will pay off in six months.
You are developing a native addon for video processing (CPU-intensive). The `processFrame()` function takes 100ms per frame. It is called in JavaScript like this: ```javascript for (let i = 0; i < 300; i++) { addon.processFrame(frames[i]); } ``` What will happen to the Event Loop?
NAN (Native Abstractions for Node.js)
**NAN (Native Abstractions for Node.js)** is a C++ wrapper library that was created BEFORE the appearance of N-API to solve the same problem: isolating addons from changes in the V8 API. NAN provides macros and helpers that compile into different code depending on the version of Node.js. This allowed writing an addon once, but rebuilding it for each version of Node.js.
Imagine a power adapter. You travel to different countries (Node.js versions), where there are different sockets (V8 API), but you take a universal adapter (NAN). It doesn't solve the problem of rebuilding, but at least you don't have to rewrite all the code for each version. NAN was the standard from 2014-2018, but with the advent of N-API (2018+), it became a **legacy technology**.
**Why NAN is still encountered:** Many old popular libraries are written using NAN: `node-sass` (deprecated, replaced by `sass`), `canvas`, `serialport`, `node-hid`. Reasons: 1. **Legacy code** - the library works, and there are no resources for migration 2. **V8-specific features** - NAN provides access to low-level V8 APIs that are not available in N-API 3. **Inertia** - old tutorials and examples use NAN But for **new projects**, using NAN is NOT recommended - use N-API (directly or through the `node-addon-api` C++ wrapper).
The real problem with NAN: node-sass and Node.js 16
**History:** `node-sass` is a popular SASS compiler (a wrapper over libsass) written in NAN. In 2021, Node.js 16 was released with a new version of V8. **What happened:** 1. Developers updated Node.js to 16 2. `npm install` started failing with the error: `node-sass incompatible with Node.js 16` 3. Prebuilt binaries for Node.js 16 have not yet been released 4. Attempt to rebuild from source: `gyp ERR! C++ compilation failed` 5. Thousands of projects broke when trying to update Node.js **Solution:** 1. Wait for the release of `node-sass` with binaries for Node.js 16 (a few weeks) 2. Roll back to Node.js 14 (unacceptable for security updates) 3. Migrate to `sass` (Dart implementation, without native dependencies) **Conclusion:** NAN does not solve the ABI stability problem. Every new version of Node.js = waiting for the rebuild of all NAN libraries. N-API solves this **completely**.
**NAN deprecated for new projects:** - ❌ Do not use NAN for new addons - ❌ Do not use tutorials mentioning NAN (outdated) - ✅ Use N-API directly or through `node-addon-api` (C++ wrapper) - ✅ Migrate existing NAN addons to N-API **Exceptions:** If you need V8-specific features (HandleScope, Isolate, EmbedderData) that are not available in N-API - use the direct V8 API, but be prepared for breaking changes.
You found a library on npm for working with USB devices. In the package.json dependencies: `"nan": "^2.14.0"`. The library is popular, but the last release was 2 years ago. What are the risks?
node-gyp: Building addons
**node-gyp** is a tool for compiling native addons from C++ sources into a `.node` file (dynamic library). It is a wrapper around **GYP (Generate Your Projects)** - a build system developed by Google for Chromium. node-gyp reads the `binding.gyp` file (project configuration), generates a Makefile (Linux/macOS) or Visual Studio project (Windows), and initiates the compilation.
Imagine you are building a house. C++ code is the blueprints. node-gyp is the foreman who: 1. Reads the blueprints (`binding.gyp`) 2. Finds the necessary tools (C++ compiler, Python, Node.js headers) 3. Organizes the construction (compilation) 4. Assembles the finished house (`.node` file) node-gyp is a **low-level** tool. A regular developer does not call it directly: `npm install` automatically runs node-gyp for native dependencies.
**What happens during `npm install` of a native addon:** 1. npm downloads the package from the registry 2. Checks if there is a prebuilt binary (via `node-pre-gyp` or `prebuildify`) 3. If a binary is found - it unpacks and is ready 4. If not - it runs `node-gyp rebuild`: - Downloads Node.js headers (needed for compilation) - Reads `binding.gyp` - Runs the compiler (g++, clang, MSVC) - Links with Node.js/V8 libraries - Saves the result in `build/Release/addon.node` 5. When `require('addon')` is called, Node.js loads the compiled `.node` file This explains why installing `bcrypt` takes 30 seconds, while `lodash` is instant.
Typical issues with node-gyp and how to solve them
**Problem 1: `gyp ERR! find Python`** node-gyp requires Python 2.7 or 3.x. On new macOS/Windows, Python may be missing. **Solution:** ```bash # macOS brew install python # Windows npm install --global windows-build-tools # Installs Python + MSVC # Linux (Debian/Ubuntu) sudo apt-get install python3 build-essential ``` **Problem 2: `gyp ERR! stack Error: not found: make`** C++ compiler is missing. **Solution:** ```bash # macOS xcode-select --install # Ubuntu/Debian sudo apt-get install build-essential # Windows npm install --global windows-build-tools ``` **Problem 3: `fatal error: node.h: No such file or directory`** Node.js headers were not downloaded (network issue). **Solution:** ```bash # Explicitly download headers node-gyp install # Or specify local headers node-gyp rebuild --nodedir=/path/to/node/source ``` **Problem 4: Build fails on CI/CD** Long build time or missing dependencies. **Solution:** Use prebuilt binaries: ```json { "dependencies": { "node-pre-gyp": "^1.0.0" }, "binary": { "module_name": "addon", "module_path": "./lib/binding/", "host": "https://github.com/user/repo/releases/download/" } } ``` Publish prebuilt binaries for popular platforms (Linux x64, macOS arm64, Windows x64) - users will download ready-made ones without compiling.
**Why node-gyp is a pain for the ecosystem:** 1. **Requires toolchain:** Python, C++ compiler, make/MSBuild. Often missing on clean Windows/macOS. 2. **Long build time:** C++ compilation can take minutes. Multiply by the number of native dependencies. 3. **Fails without clear messages:** `gyp ERR! stack Error` - what exactly broke? 4. **Problems on CI/CD:** Docker images often lack build-tools. Alpine Linux is particularly problematic (musl libc vs glibc). 5. **Debugging complexity:** If the build fails with a C++ error, C++ knowledge is needed to fix it. **Solutions:** - **Prebuilt binaries** - publish compiled versions for popular platforms - **N-API** - reduces the frequency of rebuilds - **WASM** - an alternative to native addons for some scenarios (e.g., image processing via wasm-imagemagick) - **Pure JS alternatives** - if performance is acceptable, avoid native dependencies
Memory Safety: Memory Management
In JavaScript, you are used to automatic memory management: you create an object - V8 GC will clean it up itself when it's no longer needed. In the C++ world of native addons, you are in **manual mode**: you allocate memory using `malloc()`/`new` - you must free it using `free()`/`delete`. Forgot to free it - memory leak. Freed it twice - segfault and process crash.
Moreover, there are **two heaps**: JavaScript heap (managed by V8 GC) and native heap (managed manually in C++). N-API provides mechanisms for their synchronization: when a JS object is deleted by the GC, the associated C++ resource must also be freed. This is critical for working with files, sockets, GPU memory - if not freed, the leak will grow to OOM.
**Why memory safety is critical in production:** JavaScript server with a memory leak in pure JS: The GC will eventually catch the objects (if there are no cyclic references). Worst case - restart once a week. Native addon with a memory leak: leak in the native heap, GC doesn't see it, grows to OOM. The server crashes in a few hours. In production, this means: - Night alerts - Lost transactions - Unhappy users - Complex debugging (where exactly is the leak?) One error in C++ code can kill the stability of the entire service.
Real bug: memory leak in canvas addon
**History:** The `node-canvas` library (Canvas API for Node.js) used the Cairo library for rendering. In version 1.x there was a bug: **Code:** ```cpp Canvas* CreateCanvas(int width, int height) { cairo_surface_t* surface = cairo_image_surface_create( CAIRO_FORMAT_ARGB32, width, height ); // BUG: surface is created but never destroyed // when the JS Canvas object is deleted return new Canvas(surface); } ``` **Symptoms:** - The server generates thumbnails for uploaded images - Memory grows by 5MB every 100 requests - After a day, the process occupies 10GB and crashes with OOM - `node --inspect` + heap snapshot: native memory is not visible! **Solution:** ```cpp void CanvasFinalizer(napi_env env, void* data, void* hint) { Canvas* canvas = static_cast<Canvas*>(data); cairo_surface_destroy(canvas->surface); // Free Cairo resource delete canvas; } // When creating Canvas, add a finalizer: napi_wrap(env, js_canvas, canvas, CanvasFinalizer, nullptr, nullptr); ``` **Conclusion:** Every C++ resource that is not automatically managed (files, sockets, GPU memory, C++ libraries) MUST have a finalizer for cleanup.
**Tools for detecting memory leaks in native addons:** 1. **Valgrind (Linux):** ```bash valgrind --leak-check=full --show-leak-kinds=all node app.js ``` Displays all memory leaks with stack trace. 2. **AddressSanitizer (ASAN):** ```bash export ASAN_OPTIONS=detect_leaks=1 node --expose-gc app.js ``` Faster and more accurate than Valgrind. 3. **Chrome DevTools Memory Profiler:** Only sees JS heap, does not see native memory. Needs to be used in combination with process.memoryUsage().external. 4. **process.memoryUsage() monitoring:** ```javascript setInterval(() => { const mem = process.memoryUsage(); console.log({ rss: (mem.rss / 1024 / 1024).toFixed(2) + ' MB', // Total heapUsed: (mem.heapUsed / 1024 / 1024).toFixed(2), // JS heap external: (mem.external / 1024 / 1024).toFixed(2) // Native memory }); }, 5000); ``` If `external` grows - there's a leak in the native addon. 5. **Linux /proc/PID/smaps:** Detailed information about process memory by regions.
Async Workers and Best Practices
The main rule of native addons: **DO NOT block the Event Loop**. If your C++ function does something for more than a few milliseconds (image processing, cryptography, parsing large files) - it MUST be asynchronous. N-API provides the **AsyncWorker** pattern for executing C++ code in a background thread without blocking JavaScript.
Imagine a restaurant. The waiter (Event Loop) should not go to the kitchen and cook the dish himself - he would block the service of other tables. Instead, he passes the order to the chef (AsyncWorker), continues to serve customers, and when the dish is ready, the chef rings the bell (callback), and the waiter brings the dish. This is how async native addons work.
**node-addon-api vs pure N-API:** **node-addon-api** is a C++ wrapper over N-API, providing: - ✅ RAII (automatic resource management) - ✅ C++ exceptions instead of napi_status codes - ✅ Convenient classes (AsyncWorker, ObjectWrap, Promise) - ✅ Less boilerplate code **Pure N-API** - C API: - ✅ Works with C projects (not just C++) - ✅ Slightly less overhead (but the difference is negligible) - ❌ More code (manual napi_status checking) - ❌ No automatic resource management **Recommendation:** For C++ projects, use **node-addon-api** - the code is cleaner and safer. For C projects - pure N-API.
Real pattern: sharp (image processing)
`sharp` - a popular image processing library using libvips (C++ library). Architecture: **JavaScript API:** ```javascript sharp('input.jpg') .resize(300, 200) .toFile('output.jpg', (err, info) => { console.log('Done!'); }); ``` **Under the hood (simplified):** 1. `sharp('input.jpg')` - creates a Pipeline object (JavaScript) 2. `.resize(300, 200)` - adds an operation to the pipeline (JavaScript) 3. `.toFile()` - creates an AsyncWorker: ```cpp class ProcessImageWorker : public Napi::AsyncWorker { void Execute() override { // Executes in thread pool (does not block Event Loop) VipsImage* image; vips_image_new_from_file(input_path, &image); // Loading vips_resize(image, &resized, scale); // Resize vips_image_write_to_file(resized, output_path); // Saving g_object_unref(image); } void OnOK() override { // Callback in the main thread Callback().Call({ Env().Null(), result_info }); } }; ``` 4. Event Loop is free to handle other requests 5. Callback is called upon completion **Why it's fast:** - libvips uses SIMD instructions (AVX2) for pixel processing - Streaming: processes the image in chunks, without loading everything into memory - Multi-threading: libvips uses all CPU cores internally - Does not block Node.js Event Loop **Result:** sharp processes 1000 images/second on a regular server, 10x faster than pure-JS counterparts.
**When a native addon is NOT needed:** ❌ **Simple calculations** - Worker Threads are sufficient: ```javascript const { Worker } = require('worker_threads'); const worker = new Worker('./heavy-calc.js'); // Simpler than writing C++ ``` ❌ **I/O operations** - async Node.js API is already optimal: ```javascript fs.readFile('file.txt', callback); // Uses libuv, non-blocking ``` ❌ **JSON parsing** - V8 is optimized for this: ```javascript JSON.parse(data); // Native implementation in V8 ``` ✅ **When it IS needed:** - Integration with existing C/C++ libraries (OpenCV, TensorFlow) - CPU-intensive tasks where maximum performance is needed (cryptography, image processing) - Access to system APIs not available from JS (USB, Bluetooth, low-level network) - Working with GPU via CUDA/OpenCL **Rule:** First profile, ensure there is a real bottleneck, try Worker Threads, and only then write a native addon.
Key Ideas
- **N-API (Node-API) - modern standard:** ABI stable between Node.js versions, an addon compiled once works on Node.js 10-20+ without recompilation. NAN - legacy technology, do not use for new projects.
- **node-gyp - the ecosystem's pain:** requires Python, C++ compiler, long build times. Solution - prebuilt binaries for popular platforms (node-pre-gyp, prebuildify) and migration to N-API for compatibility.
- **Memory safety is critical:** C++ error = segfault = process crash. Use RAII (smart pointers), finalizers for C++ resources (napi_wrap), Valgrind/ASAN for leak detection. process.memoryUsage().external for monitoring native memory.
- **AsyncWorker for long operations:** DO NOT block the Event Loop. Any operation >10ms should be executed in an AsyncWorker (separate thread). Execute() - C++ computations, OnOK() - return result to JS. Increase UV_THREADPOOL_SIZE for high load.
- **Native addon - last resort:** First, profile, try Worker Threads, and only for real CPU-bound bottlenecks write C++. For I/O and business logic, JavaScript is more efficient.
Related topics
Native Addons are a way to go beyond the JavaScript runtime. For a complete understanding, study the related concepts:
- Worker Threads — Alternative to native addons for CPU-intensive tasks. Easier development (you write in JS), but slower (no access to C++ libraries and SIMD). Try Worker Threads before writing a native addon.
- libuv and Event Loop — AsyncWorker uses the libuv thread pool to execute C++ code. Understanding the Event Loop is critical for the correct use of async addons.
- V8 Engine and Memory Management — Native addons operate at the boundary of V8 (JavaScript heap) and native heap (C++ memory). Understanding V8 GC explains why finalizers are needed and how napi_wrap works.
- WebAssembly (WASM) — A modern alternative to native addons for porting C/C++ code. Easier deployment (no node-gyp), cross-platform, but slower than native addons (no direct access to Node.js API).
Вопросы для размышления
- Your API server processes 10,000 req/sec. You added a native addon for JWT token validation (C++ library). Latency increased by 20%. What could be the problem and how to diagnose it?
- You are migrating a legacy C++ project to Node.js using a native addon. The code uses global variables and static state. What problems will arise in a multithreaded Node.js environment (Worker Threads, Cluster)?
- In which scenarios is WebAssembly (WASM) preferable to a native addon, despite lower performance? Consider deployment, security, and cross-platform compatibility.