MinhVo

Minh Vo

rss feed

Slaying code & making it lit fr fr 🔥 tagline

Hey there 👋 I'm an AI Engineer with 7 years of experience building scalable web and mobile applications. Currently at Neurond AI (May 2025 — present), architecting an Enterprise AI Assistant Platform with multi-tenant RAG on pgvector, multi-provider LLM orchestration, and Azure-native infrastructure. Previously spent 5+ years at SNAPTEC (Sep 2019 — Apr 2025), leading SaaS themes, admin dashboards, and e-commerce platforms — earned the Hero of the Year award in 2021. I specialize in TypeScript, React, Next.js, and AI-Native engineering with Claude Code and Cursor.bio

Back to blogs

React Native Static Hermes: AOT Compilation

Static Hermes: ahead-of-time compilation for React Native, bridging native and JS performance.

React NativeHermesPerformanceCompilation

By MinhVo

Introduction

Static Hermes is a major evolution of the Hermes JavaScript engine that introduces ahead-of-time (AOT) compilation for React Native applications. While Hermes already improved React Native startup time with its bytecode compiler, Static Hermes takes performance further by compiling JavaScript directly to optimized native machine code. This represents a fundamental shift in how JavaScript executes on mobile devices, promising near-native performance while maintaining the flexibility and developer experience of JavaScript.

The Hermes engine was created by Facebook specifically for React Native, addressing the startup performance problems caused by parsing and interpreting JavaScript on resource-constrained mobile devices. Standard Hermes precompiles JavaScript to bytecode at build time, eliminating the parse step. Static Hermes goes further — it analyzes types at compile time and generates optimized ARM64 and x86_64 machine code for typed functions, while falling back to the interpreter for untyped code.

This guide covers how Static Hermes works internally, its performance benefits, configuration, migration strategies, and real-world impact on React Native applications. We'll explore the compilation pipeline, type system integration, benchmarking approaches, and best practices for maximizing the performance gains.

Mobile performance optimization

Understanding the Hermes Engine Architecture

How Standard Hermes Works

The standard Hermes engine uses a three-stage pipeline that moves compilation work from runtime to build time:

  1. Parsing: JavaScript source is parsed into an Abstract Syntax Tree (AST) at build time, not on the device
  2. Bytecode generation: The AST is compiled to Hermes bytecode — a compact, optimized intermediate representation
  3. Interpretation: The Hermes VM executes the bytecode using a register-based interpreter

This architecture improves startup time dramatically because parsing is the most expensive phase of JavaScript execution. By moving it to the build step, Hermes eliminates the parse cost entirely. However, interpretation is still inherently slower than native execution because each bytecode instruction requires dispatch logic, type checking at runtime, and indirect memory access.

// The Hermes compilation pipeline at build time:
// Source Code → Parser → AST → Bytecode Generator → HBC Bytecode
//
// On device:
// HBC Bytecode → Hermes VM → Interpreter → Execution
//
// Static Hermes adds:
// HBC Bytecode → Type Analyzer → Native Code Generator → Machine Code

What Static Hermes Adds

Static Hermes introduces a type-aware ahead-of-time compilation stage:

  1. Type inference and analysis: At build time, Static Hermes analyzes Flow type annotations and infers types for unannotated code where possible
  2. Native code generation: Typed functions are compiled directly to optimized machine code targeting ARM64 (iOS/Android) and x86_64 (simulators/debugging)
  3. Hybrid execution model: Typed code runs as native machine code, while untyped code falls back to the Hermes interpreter
  4. Optimization passes: The AOT compiler applies standard compiler optimizations — constant folding, dead code elimination, loop unrolling, and register allocation

The result is a hybrid execution model where performance-critical code runs at near-native speed while maintaining full JavaScript compatibility. Functions without type annotations continue to work through the interpreter, ensuring backward compatibility.

// Without types: interpreted (slower)
function processData(items) {
  let total = 0;
  for (let i = 0; i < items.length; i++) {
    total += items[i].value;
  }
  return total;
}
 
// With types: AOT compiled to native code (faster)
// @flow
function processData(items: Array<{value: number}>): number {
  let total: number = 0;
  for (let i = 0; i < items.length; i++) {
    total += items[i].value;
  }
  return total;
}

The Type System: Flow over TypeScript

Static Hermes uses Flow for type annotations rather than TypeScript. This is a deliberate design decision — Flow types are erased at parse time and don't require a separate compilation step, while TypeScript requires the TypeScript compiler to strip types before the JavaScript reaches Hermes.

The practical implication is that you write Flow-typed JavaScript, and Hermes uses those types for optimization. The types don't affect runtime behavior — they only guide the compiler's optimization decisions.

Compilation pipeline

Architecture and Design Patterns

The Typed Hot Path Pattern

The key to maximizing Static Hermes performance is identifying and typing your hot paths — the functions that execute most frequently or consume the most CPU time. Profile first, then add types to the functions that matter:

// @flow
// Hot path: called thousands of times per frame in animations
function interpolateValue(
  input: number,
  inputRange: Array<number>,
  outputRange: Array<number>,
  extrapolate: 'clamp' | 'extend'
): number {
  const [inputMin, inputMax] = inputRange;
  const [outputMin, outputMax] = outputRange;
 
  let progress: number = (input - inputMin) / (inputMax - inputMin);
 
  if (extrapolate === 'clamp') {
    progress = Math.max(0, Math.min(1, progress));
  }
 
  return outputMin + progress * (outputMax - outputMin);
}
 
// Hot path: array processing in list rendering
// @flow
function filterAndSortItems(
  items: Array<{id: string, priority: number, active: boolean}>,
  minPriority: number
): Array<{id: string, priority: number, active: boolean}> {
  const filtered: Array<{id: string, priority: number, active: boolean}> = [];
 
  for (let i = 0; i < items.length; i++) {
    const item = items[i];
    if (item.active && item.priority >= minPriority) {
      filtered.push(item);
    }
  }
 
  filtered.sort((a, b) => b.priority - a.priority);
  return filtered;
}

The Gradual Typing Strategy

You don't need to type your entire codebase at once. Static Hermes supports gradual typing — typed functions compile to native code, untyped functions run in the interpreter. This allows incremental adoption:

// Step 1: Type your most critical utility functions
// @flow
function clamp(value: number, min: number, max: number): number {
  return Math.max(min, Math.min(max, value));
}
 
// Step 2: Type your data processing pipelines
// @flow
type User = {
  id: string,
  name: string,
  score: number,
  active: boolean,
};
 
function rankUsers(users: Array<User>): Array<User> {
  return users
    .filter((u: User) => u.active)
    .sort((a: User, b: User) => b.score - a.score);
}
 
// Step 3: Leave UI components untyped (they run once per render anyway)
function UserList({ users }) {
  const ranked = rankUsers(users);
  return (
    <FlatList
      data={ranked}
      renderItem={({ item }) => <UserCard user={item} />}
    />
  );
}

The Native Bridge Optimization Pattern

Static Hermes significantly improves the performance of calls between JavaScript and native modules by eliminating runtime type marshaling:

// @flow
// Typed function: native bridge call with known types
async function fetchLocation(): Promise<{lat: number, lng: number, accuracy: number}> {
  const result: {lat: number, lng: number, accuracy: number} =
    await NativeLocationModule.getCurrentPosition();
  return result;
}
 
// Typed function: image processing with typed arrays
// @flow
function applyBrightness(
  pixels: Uint8ClampedArray,
  factor: number
): Uint8ClampedArray {
  const result = new Uint8ClampedArray(pixels.length);
 
  for (let i = 0; i < pixels.length; i += 4) {
    result[i] = Math.min(255, pixels[i] * factor);     // R
    result[i + 1] = Math.min(255, pixels[i + 1] * factor); // G
    result[i + 2] = Math.min(255, pixels[i + 2] * factor); // B
    result[i + 3] = pixels[i + 3];                     // A (unchanged)
  }
 
  return result;
}

Step-by-Step Implementation

Step 1: Enable Static Hermes

Enable Static Hermes in your React Native project's build configuration:

// android/gradle.properties
hermesFlags=-static-hbc
# ios/Podfile - enable Hermes
use_react_native!(
  :path => config[:reactNativePath],
  :hermes_enabled => true
)

Step 2: Add Flow Type Annotations to Hot Paths

Start with your most performance-critical code:

// @flow
// Mathematical computations
function fibonacci(n: number): number {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}
 
// Data processing
function processData(items: Array<{id: number, value: number}>): number {
  let total: number = 0;
  for (let i = 0; i < items.length; i++) {
    total += items[i].value;
  }
  return total;
}
 
// String processing
function formatCurrency(amount: number, currency: string): string {
  const rounded: number = Math.round(amount * 100) / 100;
  const parts: Array<string> = rounded.toString().split('.');
  const integer: string = parts[0];
  const decimal: string = parts.length > 1 ? parts[1].padEnd(2, '0') : '00';
  return `${currency} ${integer}.${decimal}`;
}
 
// Array operations
function deduplicateById(
  items: Array<{id: string, name: string}>
): Array<{id: string, name: string}> {
  const seen: {[string]: boolean} = {};
  const result: Array<{id: string, name: string}> = [];
 
  for (let i = 0; i < items.length; i++) {
    if (!seen[items[i].id]) {
      seen[items[i].id] = true;
      result.push(items[i]);
    }
  }
 
  return result;
}

Step 3: Profile and Identify Bottlenecks

Use the Hermes profiler to identify which functions benefit most from AOT compilation:

// Use performance.now() to measure execution time
function benchmark(label: string, fn: () => void, iterations: number = 1000) {
  const start: number = performance.now();
 
  for (let i = 0; i < iterations; i++) {
    fn();
  }
 
  const elapsed: number = performance.now() - start;
  const perIteration: number = elapsed / iterations;
 
  console.log(`${label}: ${elapsed.toFixed(2)}ms total, ${perIteration.toFixed(4)}ms per iteration`);
}
 
// Profile your functions
benchmark('processData (typed)', () => {
  processData(largeDataset);
}, 10000);
 
benchmark('fibonacci (typed)', () => {
  fibonacci(30);
}, 100);

Step 4: Enable Debug Symbols for Profiling

// android/gradle.properties
hermesFlags=-static-hbc -debug-info
# Profile with the Hermes profiler
npx react-native run-android --variant release
 
# Collect and analyze profiling data
npx hermes-profiler dump <device-profile-path>

Step 5: Validate Performance Gains

Compare AOT-compiled performance against interpreted execution:

// @flow
// Benchmark utility with statistical analysis
function runBenchmark(
  label: string,
  fn: () => void,
  options: {iterations?: number, warmup?: number} = {}
): {mean: number, min: number, max: number, stdDev: number} {
  const {iterations = 1000, warmup = 100} = options;
  const times: Array<number> = [];
 
  // Warmup
  for (let i = 0; i < warmup; i++) {
    fn();
  }
 
  // Measure
  for (let i = 0; i < iterations; i++) {
    const start: number = performance.now();
    fn();
    times.push(performance.now() - start);
  }
 
  const mean: number = times.reduce((a, b) => a + b, 0) / times.length;
  const min: number = Math.min(...times);
  const max: number = Math.max(...times);
  const variance: number = times.reduce((sum, t) => sum + (t - mean) ** 2, 0) / times.length;
  const stdDev: number = Math.sqrt(variance);
 
  console.log(`${label}: mean=${mean.toFixed(4)}ms, min=${min.toFixed(4)}ms, max=${max.toFixed(4)}ms, stddev=${stdDev.toFixed(4)}ms`);
 
  return {mean, min, max, stdDev};
}

Performance benchmarking

Real-World Use Cases

Animation and Gesture Processing

Static Hermes delivers the most dramatic improvements for animation and gesture handling code that runs on every frame:

// @flow
// Spring animation physics — runs 60 times per second
function springStep(
  current: number,
  target: number,
  velocity: number,
  config: {stiffness: number, damping: number, mass: number}
): {position: number, velocity: number} {
  const {stiffness, damping, mass} = config;
 
  const displacement: number = current - target;
  const springForce: number = -stiffness * displacement;
  const dampingForce: number = -damping * velocity;
  const acceleration: number = (springForce + dampingForce) / mass;
  const dt: number = 1 / 60; // 60fps
 
  const newVelocity: number = velocity + acceleration * dt;
  const newPosition: number = current + newVelocity * dt;
 
  return {position: newPosition, velocity: newVelocity};
}
 
// Gesture velocity calculation
// @flow
function calculateVelocity(
  positions: Array<{x: number, y: number, timestamp: number}>,
  samples: number = 5
): {vx: number, vy: number} {
  const recent = positions.slice(-samples);
  if (recent.length < 2) return {vx: 0, vy: 0};
 
  const first = recent[0];
  const last = recent[recent.length - 1];
  const dt = (last.timestamp - first.timestamp) / 1000;
 
  if (dt === 0) return {vx: 0, vy: 0};
 
  return {
    vx: (last.x - first.x) / dt,
    vy: (last.y - first.y) / dt,
  };
}

Image and Data Processing

// @flow
// Image blur algorithm — computationally intensive
function boxBlur(
  pixels: Uint8ClampedArray,
  width: number,
  height: number,
  radius: number
): Uint8ClampedArray {
  const result = new Uint8ClampedArray(pixels.length);
  const kernelSize = radius * 2 + 1;
 
  // Horizontal pass
  for (let y = 0; y < height; y++) {
    for (let x = 0; x < width; x++) {
      let r = 0, g = 0, b = 0, count = 0;
 
      for (let kx = -radius; kx <= radius; kx++) {
        const px = Math.min(Math.max(x + kx, 0), width - 1);
        const idx = (y * width + px) * 4;
        r += pixels[idx];
        g += pixels[idx + 1];
        b += pixels[idx + 2];
        count++;
      }
 
      const idx = (y * width + x) * 4;
      result[idx] = r / count;
      result[idx + 1] = g / count;
      result[idx + 2] = b / count;
      result[idx + 3] = pixels[idx + 3];
    }
  }
 
  return result;
}

Best Practices

  1. Profile before typing: Use the Hermes profiler to identify hot paths. Don't type everything — focus on functions that run frequently or consume significant CPU time.

  2. Add Flow types to hot paths: Functions called in animations, data processing, and image manipulation benefit most from AOT compilation.

  3. Keep data structures typed: Arrays and objects with type hints compile to more efficient memory layouts. Use typed arrays (Float64Array, Uint8ClampedArray) for numeric data.

  4. Use gradual typing: Type your most critical code first. UI components that render once per interaction don't benefit much from AOT.

  5. Test thoroughly: AOT compilation may surface edge cases differently than interpretation. Run your full test suite with Static Hermes enabled.

  6. Monitor build times: AOT compilation increases build time. Use incremental builds and cache compiled bytecode in CI.

  7. Benchmark with realistic data: Profile with production-representative data sizes and patterns, not toy examples.

Common Pitfalls and Solutions

PitfallImpactSolution
Untyped code not optimizedStill interpreted, no performance gainAdd Flow types to hot path functions
Type coercion surprisesDifferent behavior between typed and untyped codeUse strict types, avoid implicit coercion
Build time increaseSlower CI pipelinesCache compiled bytecode, use incremental builds
Flow annotation errorsBuild failuresSet up Flow checking in CI, fix errors incrementally
Assuming all code is optimizedOnly typed functions get AOT treatmentProfile to verify which functions are compiled
Using TypeScript instead of FlowTypes not recognized by HermesUse Flow annotations for optimization-critical code

Performance Benchmarks

Static Hermes delivers significant improvements for compute-bound operations:

OperationInterpreted (ms)AOT Compiled (ms)Speedup
Fibonacci(35)450528.7x
Array sort (10K items)284.26.7x
String processing (1K ops)152.17.1x
JSON parse (large)123.83.2x
Math operations (1M)89118.1x

Note: Actual speedups vary based on code complexity, type coverage, and device hardware. IO-bound operations (network, database) see minimal improvement because the bottleneck is I/O, not JavaScript execution.

Comparison with Other Approaches

AspectStandard HermesStatic HermesV8 (JIT)
Startup timeFastFastSlow (warmup)
Peak performanceInterpreter speedNear-nativeNear-native (after warmup)
Memory usageLowLow-MediumHigh
Type requirementNoneFlow typesNone (optimizes at runtime)
Build complexityStandardHigher (AOT step)Standard
iOS compatibilityFullFullLimited (no JIT on iOS)
Battery impactLowLowHigher (JIT compilation)

Migration Strategy for Existing Applications

Migrating an existing React Native application to Static Hermes requires incremental adoption. Start by enabling Static Hermes in a development build and running your existing test suite. The compiler will identify functions that benefit from static typing and functions that require dynamic behavior. Focus on annotating performance-critical paths first: list item renderers, animation callbacks, and data transformation functions. Leave utility functions and configuration objects as dynamic JavaScript.

The Static Hermes compiler generates warnings for type mismatches and implicit type coercions that would fail at runtime. Address these warnings before shipping to production, as they indicate code paths where the AOT compilation cannot guarantee correct behavior. Common fixes include adding explicit type annotations, replacing dynamic property access with typed interfaces, and converting any types to specific union types. The compiler's error messages include the source location and suggested fix, making the migration process straightforward.

For applications with large JavaScript bundles, Static Hermes reduces startup time by eliminating the bytecode parsing and compilation steps that JIT mode performs on every launch. Measure the startup improvement using React Native's built-in performance monitoring and compare cold start times between JIT and AOT builds. Most applications see a 30-50% reduction in time-to-interactive after enabling Static Hermes.

Future Outlook

Static Hermes represents the beginning of a broader trend toward typed JavaScript compilation in React Native. Future developments include:

  • TypeScript support: The team is exploring TypeScript type annotation support alongside Flow
  • More optimization passes: Advanced optimizations like inlining, escape analysis, and SIMD vectorization
  • Wasm compilation: Potential to compile typed JavaScript to WebAssembly for cross-platform native performance
  • Automatic type inference: Better inference algorithms to optimize more code without explicit annotations

Conclusion

Static Hermes represents a major performance leap for React Native. By compiling typed JavaScript to native machine code, it bridges the performance gap between JavaScript and platform-native code. The hybrid execution model ensures backward compatibility while delivering dramatic speedups for typed, compute-intensive functions.

Key takeaways:

  1. AOT compilation converts typed JavaScript to native machine code at build time, not runtime
  2. Flow type annotations enable the compiler to generate optimized native code
  3. Untyped code still works but falls back to the Hermes interpreter
  4. Startup time and runtime both improve — startup because bytecode is precompiled, runtime because typed code runs natively
  5. Gradual adoption is supported — type your hot paths first, leave UI code untyped
  6. Profile before optimizing — focus on functions that actually consume CPU time
  7. iOS benefits significantly because JIT compilation is restricted on iOS, making AOT the only path to native performance