Skip to content

Instantly share code, notes, and snippets.

@schickling
Created December 16, 2025 14:18
Show Gist options
  • Select an option

  • Save schickling/e53f0e65b772c0e62492c99afe3ae2cf to your computer and use it in GitHub Desktop.

Select an option

Save schickling/e53f0e65b772c0e62492c99afe3ae2cf to your computer and use it in GitHub Desktop.
Static Hermes + Effect Framework Compatibility Report - December 2025

Static Hermes + Effect Experiment

This experiment tests the compatibility of Effect framework with Static Hermes, Facebook's ahead-of-time JavaScript compiler.

Summary

Effect works with Static Hermes in both interpreted and native binary compilation modes. This enables Effect-based applications to be compiled to standalone native executables.

Setup

Prerequisites (via Nix)

nix develop  # Activates the development shell with all dependencies

Building Static Hermes

git clone --depth 1 --branch static_h https://github.com/facebook/hermes.git hermes-static
cmake -S hermes-static -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build ./build

Running Effect Code

Bundle Effect code with polyfills, then run with shermes:

# Bundle with esbuild
node_modules/.bin/esbuild ./my-app.ts --bundle --outfile=./dist/app.js \
  --platform=neutral --format=esm --inject:./polyfills.js

# Run interpreted
LIBRARY_PATH=./build/tools/shermes ./build/bin/shermes -exec ./dist/app.js

# Compile to native binary
LIBRARY_PATH=./build/tools/shermes ./build/bin/shermes -o my-app-binary ./dist/app.js
./my-app-binary

Required Polyfills

Static Hermes runtime is minimal and lacks some Web APIs. The following polyfills are needed:

// polyfills.js
if (typeof globalThis.URL === "undefined") {
  globalThis.URL = function URL(url, base) { this.href = base ? base + url : url; };
}
if (typeof globalThis.TextDecoder === "undefined") {
  globalThis.TextDecoder = function TextDecoder() {};
  globalThis.TextDecoder.prototype.decode = function(bytes) {
    if (!bytes) return "";
    var result = "";
    for (var i = 0; i < bytes.length; i++) {
      result += String.fromCharCode(bytes[i]);
    }
    return result;
  };
}
if (typeof globalThis.TextEncoder === "undefined") {
  globalThis.TextEncoder = function TextEncoder() {};
  globalThis.TextEncoder.prototype.encode = function(str) {
    var result = new Uint8Array(str.length);
    for (var i = 0; i < str.length; i++) {
      result[i] = str.charCodeAt(i);
    }
    return result;
  };
}

Tested Effect Features

Feature Status Notes
Effect.succeed/fail ✅ Works
Effect.map/flatMap ✅ Works
Effect.gen ✅ Works Generator-based syntax
Effect.all ✅ Works Parallel execution
Effect.forEach ✅ Works
Effect.catchAll ✅ Works Error recovery
Effect.tap ✅ Works Side effects
Option ✅ Works
Either ✅ Works
Context/Layer ✅ Works Dependency injection
Schema ✅ Works Requires TextDecoder polyfill
Stream ✅ Works
runPromise ✅ Works Promise available in shermes

Performance Comparison

Test Node.js shermes (interpreted) shermes (native)
Pure JS loop (100k iterations) 3ms 3ms 2ms
Effect.succeed chain (10k) 11ms 36ms 31ms
Effect.gen chain (10k) 10ms 32ms 35ms
Effect.all (10k) 11ms 53ms 56ms

Note: Static Hermes is currently ~3x slower than Node.js for Effect operations. This is expected since:

  1. Static Hermes's typed compilation mode doesn't work with bundled Effect code (uses dynamic JS patterns)
  2. The untyped native compilation provides modest improvements over interpreted mode
  3. V8 (Node.js) is highly optimized for JavaScript execution

Limitations

Typed Mode Not Compatible

Static Hermes's -typed flag (for maximum performance with type annotations) is not compatible with bundled Effect code because Effect uses:

  • Computed property names in classes ([Symbol.iterator]())
  • Dynamic property access patterns
  • Complex type inference patterns
  • Private class fields
  • Optional chaining with calls

To use typed mode, you would need Effect to be rewritten with simpler patterns - which defeats the purpose of Effect's expressive API.

Missing Runtime APIs

The following need polyfills:

  • URL - Used by Effect's hash function
  • TextDecoder/TextEncoder - Used by Schema
  • AbortController - May be needed for cancellation (not tested)

Bundle Size

Effect binaries are ~3.4MB due to the Hermes runtime being linked in. This could potentially be optimized.

Use Cases

Static Hermes + Effect could be interesting for:

  1. CLI Tools: Single-binary distribution without Node.js dependency
  2. Embedded Systems: Effect in resource-constrained environments
  3. React Native: (Native integration - the original use case for Hermes)
  4. Edge Computing: Lightweight Effect executables for edge functions

Conclusion

Effect is compatible with Static Hermes when bundled with appropriate polyfills. While performance is currently slower than Node.js, the ability to compile Effect programs to standalone native binaries opens interesting deployment possibilities.

For production use, consider:

  • Node.js for best performance
  • Static Hermes for deployment simplicity (single binary, no runtime dependency)
  • Bun/Deno as middle ground options

References

// Polyfills for Static Hermes runtime
if (typeof globalThis.URL === "undefined") {
globalThis.URL = function URL(url, base) { this.href = base ? base + url : url; };
}
if (typeof globalThis.TextDecoder === "undefined") {
globalThis.TextDecoder = function TextDecoder() {};
globalThis.TextDecoder.prototype.decode = function(bytes) {
if (!bytes) return "";
var result = "";
for (var i = 0; i < bytes.length; i++) {
result += String.fromCharCode(bytes[i]);
}
return result;
};
}
if (typeof globalThis.TextEncoder === "undefined") {
globalThis.TextEncoder = function TextEncoder() {};
globalThis.TextEncoder.prototype.encode = function(str) {
var result = new Uint8Array(str.length);
for (var i = 0; i < str.length; i++) {
result[i] = str.charCodeAt(i);
}
return result;
};
}

Static Hermes + Effect Framework Compatibility Report

Date: December 2025 Author: Automated experiment Static Hermes Branch: static_h Effect Version: 3.19.12

Executive Summary

This report documents a comprehensive compatibility experiment between the Effect framework and Static Hermes, Facebook's ahead-of-time JavaScript compiler.

Key Finding: Effect works with Static Hermes in untyped compilation mode, enabling Effect-based applications to be compiled to standalone native executables (~3.4MB). However, the typed compilation mode (which provides maximum performance) is incompatible due to Effect's use of advanced JavaScript patterns.


Table of Contents

  1. Background
  2. Experiment Setup
  3. Test Results
  4. Performance Analysis
  5. Detailed Limitations
  6. Required Polyfills
  7. Relevant GitHub Issues
  8. Recommendations
  9. Conclusion

Background

What is Static Hermes?

Static Hermes is an evolution of Facebook's Hermes JavaScript engine. While standard Hermes compiles JavaScript to bytecode, Static Hermes can:

  1. Interpret JavaScript directly
  2. Compile to native binary (untyped mode) - compiles JS to C, then to native code
  3. Compile with type information (typed mode) - uses TypeScript/Flow annotations for maximum optimization

The typed mode promises "predictable performance on par with C/C++" but requires strict type annotations and avoids dynamic JavaScript patterns.

Why This Matters for Effect

Effect is a comprehensive TypeScript library for building type-safe, composable applications. If Effect could run on Static Hermes, it would enable:

  • Single-binary CLI tools without Node.js dependency
  • Embedded systems running Effect in resource-constrained environments
  • React Native applications with native Effect performance
  • Edge computing with lightweight Effect executables

Experiment Setup

Environment

Platform: macOS (Darwin 24.6.0, arm64)
Static Hermes: Built from static_h branch (December 2025)
Effect: 3.19.12
Bundler: esbuild 0.24.2
Build Tools: cmake 4.1.2, ninja 1.13.1, clang 21.1.2 (via Nix)

Build Process

# Clone Static Hermes
git clone --depth 1 --branch static_h https://github.com/facebook/hermes.git hermes-static

# Build with CMake + Ninja
cmake -S hermes-static -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build ./build

# Key binaries produced:
# - build/bin/shermes (Static Hermes compiler)
# - build/tools/shermes/libshermes_console.dylib (runtime library)

Test Methodology

  1. Write TypeScript test files using various Effect features
  2. Bundle with esbuild (platform=neutral, format=esm)
  3. Inject polyfills via esbuild banner/inject
  4. Run with shermes in three modes:
    • Interpreted: shermes -exec bundle.js
    • Native binary: shermes -o binary bundle.js && ./binary
    • Typed (attempted): shermes -typed -exec bundle.js

Test Results

✅ Features That Work

Feature Test Result Notes
Core Effect
Effect.succeed Basic value wrapping ✅ Pass
Effect.fail Error creation ✅ Pass
Effect.map Value transformation ✅ Pass
Effect.flatMap Monadic composition ✅ Pass
Effect.gen Generator syntax ✅ Pass Most common pattern
Effect.all Parallel execution ✅ Pass
Effect.forEach Collection iteration ✅ Pass
Effect.tap Side effects ✅ Pass
Effect.catchAll Error recovery ✅ Pass
Effect.either Error to Either ✅ Pass
Effect.runPromise Execution ✅ Pass Promise available
Data Types
Option.some/none Optional values ✅ Pass
Option.map/flatMap Transformations ✅ Pass
Option.getOrElse Default values ✅ Pass
Either.right/left Either values ✅ Pass
Either.map Transformations ✅ Pass
Chunk Immutable arrays ✅ Pass
Services & Layers
Context.Tag Service definition ✅ Pass
Layer.succeed Simple layer ✅ Pass
Layer.effect Effectful layer ✅ Pass
Layer.provide Dependency injection ✅ Pass
Layer.merge Layer composition ✅ Pass
Schema
Schema.Struct Object schemas ✅ Pass Requires TextDecoder polyfill
Schema.String/Number Primitive schemas ✅ Pass
Schema.optional Optional fields ✅ Pass
Schema.decodeUnknownSync Sync parsing ✅ Pass
Schema.decodeUnknown Async parsing ✅ Pass
Schema.NumberFromString Transformations ✅ Pass
Stream
Stream.fromIterable Create from array ✅ Pass
Stream.map Element transformation ✅ Pass
Stream.filter Filtering ✅ Pass
Stream.flatMap Nested streams ✅ Pass
Stream.take Limit elements ✅ Pass
Stream.runFold Aggregation ✅ Pass
Stream.runCollect Collect to Chunk ✅ Pass
Stream.mapEffect Effectful mapping ✅ Pass
Stream.range Numeric ranges ✅ Pass

Test Code Examples

Basic Effect Composition

const program = pipe(
  Effect.succeed(10),
  Effect.map((x) => x * 2),
  Effect.flatMap((x) => Effect.succeed(x + 5))
)
// Result: 25 ✅

Effect.gen Pattern

const program = Effect.gen(function* () {
  const a = yield* Effect.succeed(1)
  const b = yield* Effect.succeed(a + 1)
  return b * 2
})
// Result: 4 ✅

Services & Layers

class Logger extends Context.Tag("Logger")<Logger, { log: (msg: string) => Effect.Effect<void> }>() {}

const LoggerLive = Layer.succeed(Logger, {
  log: (message) => Effect.sync(() => console.log(`[LOG] ${message}`))
})

const program = Effect.gen(function* () {
  const logger = yield* Logger
  yield* logger.log("Hello from Static Hermes!")
})

Effect.runPromise(program.pipe(Effect.provide(LoggerLive)))
// Output: [LOG] Hello from Static Hermes! ✅

Schema Validation

const UserSchema = Schema.Struct({
  id: Schema.Number,
  name: Schema.String,
  email: Schema.String,
  age: Schema.optional(Schema.Number)
})

const parseUser = Schema.decodeUnknownSync(UserSchema)
const user = parseUser({ id: 1, name: "Alice", email: "alice@example.com" })
// Result: { id: 1, name: "Alice", email: "alice@example.com" } ✅

Stream Processing

const stream = Stream.range(1, 100).pipe(
  Stream.map((x) => x * 2),
  Stream.filter((x) => x % 4 === 0),
  Stream.take(5)
)
const result = yield* Stream.runCollect(stream)
// Result: [4, 8, 12, 16, 20] ✅

Performance Analysis

Benchmark Results

Test Node.js v22 shermes (interpreted) shermes (native binary)
Pure JS loop (100k iterations) 3ms 3ms 2ms
Effect.succeed chain (10k) 11ms 36ms 31ms
Effect.gen chain (10k) 10ms 32ms 35ms
Effect.all (10k) 11ms 53ms 56ms

Analysis

  1. Pure JavaScript: Static Hermes native is slightly faster than Node.js for simple loops
  2. Effect operations: Node.js (V8) is ~3x faster than Static Hermes for Effect operations
  3. Native vs Interpreted: Native compilation provides only modest improvement (~10-15%)

Why Node.js Wins for Effect

  1. V8's JIT compiler aggressively optimizes hot paths, including Effect's monadic patterns
  2. Hidden class optimization in V8 benefits Effect's consistent object shapes
  3. Static Hermes untyped mode doesn't have type information to optimize against
  4. Effect's dynamic patterns (generators, closures, proxy-like behaviors) don't benefit from AOT compilation

Binary Size

Build Size
Effect basic test binary 3.4 MB
Effect advanced test binary 3.5 MB
Effect + Schema binary ~4.2 MB (estimated)

The binaries include the Hermes runtime, which adds ~2-3MB overhead.


Detailed Limitations

1. Typed Compilation Mode Incompatibility

The -typed flag fails with bundled Effect code. Here's why:

1.1 Computed Property Names in Classes

Error: ft: computed property names in classes are unsupported

// Effect uses this pattern extensively:
class MyClass {
  [Symbol.iterator]() { /* ... */ }  // ❌ Not supported in typed mode
  [TypeId] = TypeId;                  // ❌ Not supported in typed mode
}

Tracking: No specific issue filed; this is a known limitation of typed mode.

1.2 Private Class Fields

Error: ft: unsupported class member ClassPrivateProperty

class SomeClass {
  #privateField;  // ❌ Not supported in typed mode
}

Tracking: Private fields are an ES2022 feature; typed mode has limited modern JS support.

1.3 Optional Chaining on Calls

Error: ft: optional call expression not supported

someFunction()?.method()  // ❌ Not supported in typed mode

Related Issue: facebook/hermes#403 (fixed for regular Hermes, but typed mode has separate implementation)

1.4 Dynamic Property Access on Functions

Error: ft: named property access only allowed on objects, found untyped function

function fn() {}.constructor  // ❌ Not supported in typed mode

1.5 Complex Type Inference

Warning: local variable may be used prior to declaration, assuming 'any'

Effect's heavy use of closures and the arguments object causes typed mode to fall back to any, losing type benefits.

2. Missing Runtime APIs

Static Hermes is intentionally minimal. These APIs are missing:

API Used By Workaround
URL Effect hash function Polyfill required
TextDecoder Effect Schema Polyfill required
TextEncoder Effect Schema Polyfill required
AbortController Effect interruption Polyfill likely needed
setInterval Effect scheduling Available in shermes
setTimeout Effect scheduling Available in shermes
console Logging Available in shermes
Promise Effect runtime Available in shermes

Tracking Issues:

3. Platform Support

Platform Interpreted Native Binary
macOS (arm64) ✅ Tested ✅ Tested
macOS (x86_64) ⚠️ Likely works ⚠️ Likely works
Linux (x86_64) ✅ Reported working ✅ Reported working
Windows ⚠️ Limited ❌ Not fully supported

Tracking Issue: facebook/hermes#1247 (Windows build issues)

4. No React Native Integration (Yet)

While Hermes is the default JS engine for React Native, Static Hermes integration is not officially supported.

From maintainers: "Integration with RN is not officially supported for now. Please don't try it."

5. Library Compatibility

Existing JavaScript libraries without type annotations require typed mode to fall back to any, which defeats the performance benefits.

Tracking Issue: facebook/hermes#1349 (request for automatic type inference)


Required Polyfills

Minimal Polyfill Set

// polyfills.js - Required for Effect to work with Static Hermes

// URL polyfill (used by Effect's hash function)
if (typeof globalThis.URL === "undefined") {
  globalThis.URL = function URL(url, base) {
    this.href = base ? base + url : url;
  };
}

// TextDecoder polyfill (used by Schema)
if (typeof globalThis.TextDecoder === "undefined") {
  globalThis.TextDecoder = function TextDecoder() {};
  globalThis.TextDecoder.prototype.decode = function(bytes) {
    if (!bytes) return "";
    var result = "";
    for (var i = 0; i < bytes.length; i++) {
      result += String.fromCharCode(bytes[i]);
    }
    return result;
  };
}

// TextEncoder polyfill (used by Schema)
if (typeof globalThis.TextEncoder === "undefined") {
  globalThis.TextEncoder = function TextEncoder() {};
  globalThis.TextEncoder.prototype.encode = function(str) {
    var result = new Uint8Array(str.length);
    for (var i = 0; i < str.length; i++) {
      result[i] = str.charCodeAt(i);
    }
    return result;
  };
}

Usage with esbuild

# Inject polyfills at bundle time
esbuild ./app.ts --bundle --outfile=./dist/app.js \
  --platform=neutral --format=esm --inject:./polyfills.js

Caveats

  • The URL polyfill is minimal and doesn't implement full URL parsing
  • The TextDecoder/TextEncoder polyfills only handle ASCII; UTF-8 would require more complex implementation
  • For production use, consider using established polyfill libraries

Relevant GitHub Issues

Critical for Effect Compatibility

Issue Description Status Impact
#132 URL interface missing Open Requires polyfill
#948 TextEncoder missing Closed (implemented) Now available
#1403 TextDecoder missing Open Requires polyfill
#1072 Non-ECMAScript APIs discussion Open Future improvements

Static Hermes Specific

Issue Description Status Impact
#1137 How to try Static Hermes Open Main documentation
#1247 Windows build issues Open Platform support
#1349 Type inference for untyped JS Open Would help library compat
#1685 Static Hermes internals Open Technical deep-dive

Performance Related

Issue Description Status Impact
#495 3x perf drop vs JSC Open General performance
#1294 Array traversal regression Open Stream performance
#1684 Iterator helpers missing Open Stream compat

Recommendations

When to Use Static Hermes + Effect

Good use cases:

  • CLI tools where single-binary distribution matters more than raw performance
  • Embedded environments where Node.js isn't available
  • Experimentation and prototyping
  • Contexts where startup time is critical (native binaries start faster)

Not recommended:

  • Performance-critical applications (Node.js is 3x faster)
  • Applications using Effect's advanced typed features
  • Production React Native apps (not officially supported)

Optimization Tips

  1. Bundle aggressively: Use esbuild with tree-shaking to minimize bundle size
  2. Avoid typed mode: Stick to untyped compilation for Effect
  3. Polyfill early: Inject polyfills via esbuild banner to ensure they run first
  4. Test thoroughly: Some Edge cases may behave differently than Node.js

Future Outlook

Static Hermes is under active development. Key improvements that would benefit Effect:

  1. Automatic type inference (#1349) - Would allow typed mode to work with existing JS libraries
  2. Broader Web API support (#1072) - Reduce polyfill requirements
  3. React Native integration - Official support would validate the approach

Conclusion

Effect works with Static Hermes, enabling Effect-based TypeScript applications to be compiled to standalone native executables. This opens interesting deployment possibilities, particularly for CLI tools and embedded environments.

However, significant limitations exist:

  • 3x slower than Node.js for Effect operations
  • Typed mode incompatible due to Effect's dynamic JS patterns
  • Polyfills required for URL, TextDecoder, TextEncoder
  • Binary size overhead (~3MB minimum)

For most production use cases, Node.js remains the recommended runtime for Effect applications. Static Hermes is better suited for scenarios where deployment simplicity (single binary) outweighs performance requirements.


Appendix: Files in This Experiment

experiments/static-hermes/
├── flake.nix                    # Nix development environment
├── flake.lock                   # Nix lock file
├── package.json                 # Node dependencies
├── polyfills.js                 # Runtime polyfills
├── hermes-static/               # Cloned Hermes repo (static_h branch)
├── build/                       # CMake build output
├── dist/                        # Bundled JavaScript files
├── test-basic.ts                # Basic TypeScript test
├── test-globals.ts              # Runtime globals test
├── test-effect.ts               # Basic Effect test
├── test-effect-advanced.ts      # Advanced Effect patterns
├── test-effect-schema.ts        # Schema tests
├── test-effect-services.ts      # Layer/Service tests
├── test-effect-stream.ts        # Stream tests
├── test-perf.ts                 # Performance benchmarks
├── effect-test-binary           # Compiled native binary
├── effect-advanced-binary       # Compiled native binary
├── README.md                    # Quick start guide
└── REPORT.md                    # This report

References

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment