Node.js 26.9.0 came out on 16 September, and one line in the changelog changes what "just call a C library" means in Node: node:ffi, the module for calling native functions without writing a compiled addon, is now on by default. Up to 26.8.2 you needed --experimental-ffi to touch it. From 26.9.0 it just works, and the flag that used to turn it on now turns it off.
I downloaded 26.9.0, wrote a small C library, and spent the morning finding out what that switch actually costs and where it breaks.
It runs, with a warning. Passing the flag that used to be required now does nothing new; the module is already there. Passing --no-experimental-ffi is the only way to get the old behaviour back:
So this is a real default change, not a doc update. Anything that imports node:ffi and used to fail on older 26.x installs without the flag will now silently succeed on 26.9.0, which is worth knowing if you have version pins in CI.
node:ffi exists as an alternative to two older options: writing an N-API addon in C, or shelling out to a compiled binary. I built the addon comparison, since it's the fairer fight — both are calling into native code from a running Node process.
I compiled a tiny shared library with addi32(a, b), wrote an equivalent N-API addon, and timed five million calls to each, plus the same operation in plain JS as a baseline:
| Path | ns per call (three runs) | |---|---| | plain JS | 2.2 – 3.0 | | N-API addon | 34.1 – 35.8 | | node:ffi | 37.5 – 38.1 |
FFI is about 7-8% slower than a compiled addon doing the identical add, and both are around fifteen times the cost of staying in JS for something this trivial. That gap is the price of marshalling arguments across the JS/native boundary on every call, and it's paid whether you write C or just declare a signature.
The number that matters here isn't "FFI is slow" — 37ns is nothing on its own — it's that FFI does not beat the thing it's meant to replace. If you already have a native addon, node:ffi is not a performance upgrade over it. Its case is convenience: no node-gyp, no compiler toolchain in the deploy image, no rebuild per Node ABI version.
The add-two-numbers test is dominated by call overhead, not work. I reran it with a function that actually does something: summing ten million float64 values through a pointer.
| Path | time for 10M-element sum (three runs) | |---|---| | plain JS for loop | 16.0 – 18.4 ms | | node:ffi, pointer to Float64Array.buffer | 13.8 – 14.0 ms |
Here FFI is consistently faster, by 15-20%, because one call carries ten million elements instead of amortising fixed overhead over ten million calls. This is the actual shape FFI is good at: bulk buffer operations, not many small calls.
The opposite reading matters too. I ran a single call to a native fib(75) computed with a tight iterative loop in C, against the same loop written in JS:
Effectively identical. V8's JIT compiles a simple loop like that about as fast as gcc -O2 does, so for a single moderate computation, crossing into native code buys nothing. FFI's fixed per-call cost only pays for itself when the call count or the data volume is large enough to swallow it. A one-off "let me just call out to C for this" is usually not that case.
The docs call node:ffi "unsafe" and warn that a wrong signature can crash the process. I tested four ways to get it wrong, and got three different outcomes.
Passing a plain number where a uint64 argument is expected is rejected outright:
You have to pass a BigInt. A Number, even a small one like 10000000, is refused, not coerced.
Both of those are argument-shape checks the module does for you, and neither matches the "unsafe, can crash" framing on its own.
