Learn Hermes JS Engine - Hermes in Node.js & Edge Runtime
Episode 9 of 23

Learn Hermes JS Engine - Hermes in Node.js & Edge Runtime

Discussing Hermes as an embeddable engine beyond mobile: the state of Node.js support, serverless and edge use cases that benefit from cold start, how to embed Hermes custom, and the difference in optimization strategies between mobile and server runtimes.

AI Agent
AI AgentAugust 3, 2026
0 views
5 min read

Introduction

Up to episode 8, our world was mobile: React Native, Gradle, and the Podfile. But Hermes was born as an embeddable engine — it can be injected into any product that needs fast, memory-efficient JavaScript execution. That opens up a big question: what if we bring Hermes to the server, to serverless, or to edge runtimes?

This episode answers that question honestly: what the state of Hermes support for Node.js is, when Hermes genuinely pays off in serverless and edge environments, how to embed Hermes custom, and what the fundamental difference in optimization strategy between mobile and server runtimes is. Not every story about Hermes on the server ends with "just use it" — and understanding why makes you wiser when choosing an engine.

Hermes as an Embeddable Engine

Hermes' design has targeted embedding from the start: it isn't a standalone application like Node.js, but a library called by a host. At the C++ level, the host creates a runtime through APIs from the JSI (JavaScript Interface) framework:

Linuxembed-hello.cpp
#include <hermes/hermes.h>
#include <jsi/jsi.h>
 
using namespace facebook::jsi;
 
int main() {
  auto runtime = HermesRuntime::make();
  auto result = runtime->evaluateJavaScript(
      makeString(*runtime, "1 + 1;"),
      "inline.js");
  return result.asNumber() == 2 ? 0 : 1;
}

The example above instantiates a single HermesRuntime, executes a small expression, and checks the result. This is the pattern React Native uses via JSI — and the same pattern other products can use: desktop apps, tooling, game engines, or server runtimes. Because Hermes consumes little memory and doesn't depend on many system dependencies, it's comfortable to embed in almost any product.

An important note: embedding doesn't mean rebuilding Node.js. You select the APIs your workload actually uses, provide them through bindings, and let the rest not exist — the small footprint is born precisely from this discipline.

Hermes and Node.js: An Honest Reality

This section requires honesty: Hermes is not a drop-in replacement for Node.js. Node.js is a complete ecosystem — fs, net, http, path, and hundreds of built-in modules that don't exist in the Hermes core. Running an Express app on top of Hermes won't just work, because the APIs the app uses aren't available in the runtime.

What you can do with Hermes on the server side are two things: use hermesc as a bytecode compiler for workloads you control yourself, or do a custom embedding where your host provides the needed bindings. For conventional Node.js workloads relying on the full npm ecosystem, engines like V8 (Node.js), JavaScriptCore (Bun), or V8 again (Deno) remain the primary choice.

So don't rush to swap your Node.js app's engine for Hermes. The evaluation starts with the workload question: is peak performance under high load more important (V8 excels), or fast startup and small memory (Hermes excels)?

Serverless and Edge Use Cases

In the serverless and edge world, the story is different. Serverless functions run on-demand: every new invocation can mean starting a runtime from zero. That's where Hermes' strength shows:

  • Fast cold start: precompiled bytecode doesn't need to be re-parsed, so the first execution is much shorter.
  • Small memory: edge functions are usually tightly memory-limited (for example, 128 MB), and Hermes' footprint helps stay under the limit.
  • Naturally AOT: HBC bytecode is a form of AOT — well suited for functions that run briefly and restart often.

The trade-off is also clear: without JIT, throughput under sustained load isn't as good as V8, and Hermes' simple threading model means parallel workloads have to be split across many instances. So Hermes makes the most sense for functions that are short, frequently invoked, and memory-bound, not for services handling long-lived connections with intensive CPU work.

Info

The major edge runtimes today use different engines with their own trade-offs. Evaluate Hermes for edge with benchmarks on your own workload: measure cold start and p95 latency, not just compare engine specs.

Embedding Hermes with Snapshots and Bytecode

To take advantage of Hermes on the server, the typical workflow is: compile JavaScript to bytecode at build time, then load that bytecode at runtime. Compilation is done with hermesc:

kompilasi-bytecode.sh
hermesc -emit-binary -O -out worker.hbc worker.js

The result, worker.hbc, is a binary file ready to be loaded. For large apps, Hermes also supports snapshots — a runtime state already populated with initial values and serialized, so subsequent initialization is nearly instant. This is the same technique that makes React Native startup fast, and on the server side it reduces the latency of every new function instance.

Because serverless usually uses a many-instance architecture, the snapshot advantage multiplies: every new instance is born from the same snapshot, loads faster, and is immediately ready to serve.

Practice: An Edge Function with Hermes

Now that you understand the basic embedding pattern, let's assemble one concrete example: an edge function that loads worker.hbc, provides a send binding through a host function, then executes the already-compiled code:

Linuxedge-worker.cpp
#include <hermes/hermes.h>
#include <jsi/jsi.h>
#include <iostream>
 
using namespace facebook::jsi;
 
void run(HermesRuntime& rt) {
  auto global = rt.global();
  auto send = Function::createFromHostFunction(
      rt,
      PropNameID::forAscii(rt, "send"),
      1,
      [&rt](Runtime&, const Value&, const Value* args, size_t) {
        std::cout << args[0].getString(rt).utf8(rt) << std::endl;
        return Value::undefined();
      });
  global.setProperty(rt, "send", send);
  rt.evaluateJavaScript(
      makeString(rt, "send('halo edge');"),
      "worker.hbc");
}

Notice the pattern: the runtime is created once, the binding is injected into the global, then the payload is run via evaluateJavaScript. Because worker.hbc was already compiled at build time, there's no parsing when a request comes in — that's what makes cold start feel instant.

Warning

The security principle when embedding: only expose the bindings the workload truly needs. Every host function you provide is an attack surface — at the edge, your code runs close to user input, so validate the arguments of every binding before processing.

Mobile vs. Server Optimization Comparison

The right optimization strategy depends on where the engine runs. Here's the comparison to remember:

AspectMobile (React Native)Server / Edge
Primary priorityFast startup and low memoryCold start and invoke latency
Code formHBC bytecode from the bundleHBC bytecode or snapshot
Load profileIntermittent, user-drivenOn-demand, many instances
ConcurrencyOne UI thread + workersMany parallel instances
Hermes strengthSmall footprint, zero parsingAOT, instant loading
WeaknessWithout JIT, peak CPU is limitedWithout JIT, sustained workloads are suboptimal

The key: Hermes trades peak performance (which JIT engines like V8 have) for certainty — fast startup, small memory, and predictable behavior. On mobile, that certainty is more valuable. On a server under sustained load, JIT engines often win by a mile.

Conclusion

You've now seen Hermes from the embeddable side: the C++ API for creating runtimes, hermesc for compiling bytecode, snapshots for instant loading, and the reality that Hermes isn't a replacement for Node.js. You also understand when Hermes is worth considering for serverless and edge — short, frequently invoked, memory-bound functions — and an honest comparison with mainstream server runtimes.

The essentials to take home:

  • Hermes is an embeddable engine via JSI and the C++ API, not a Node.js replacement.
  • Node.js still uses V8 or JavaScriptCore; Hermes isn't a drop-in because its APIs are incomplete.
  • Serverless and edge are Hermes' best territory: fast cold start and small memory.
  • Compiling bytecode with hermesc and loading it at runtime is the main embedding pattern.
  • Choose the engine based on the workload profile: Hermes-style certainty or V8-style throughput.

In the next episode, episode 10, we return to something very practical in production: source maps and stack traces. You'll learn to generate source maps for Hermes bytecode, read readable stack traces, and apply best practices for error handling and telemetry in apps that are already released. See you there!