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 Hermes Engine: Performance for Mobile

Hermes JavaScript engine: startup performance, memory usage, and debugging.

React NativeHermesPerformanceMobile

By MinhVo

Introduction

The JavaScript engine powering your React Native application has a profound impact on startup time, memory consumption, and runtime performance. By default, React Native originally shipped with JavaScriptCore (JSC) — the same engine powering Safari. While functional, JSC was designed for browser environments and carries overhead that's unnecessary for mobile apps. Hermes, developed by Meta specifically for React Native, takes a fundamentally different approach: instead of compiling JavaScript at runtime using a JIT compiler, Hermes pre-compiles JavaScript to optimized bytecode during the build phase.

The results are dramatic. Apps using Hermes consistently show 30-50% faster time-to-interactive (TTI), 20-30% lower memory consumption, and significantly reduced APK/IPA sizes. In this guide, we'll explore how Hermes achieves these improvements, how to enable and configure it for maximum benefit, debugging techniques specific to Hermes bytecode, and the upcoming Static Hermes AOT compiler that promises near-native performance.

JavaScript Engine Performance

Understanding Hermes: Core Concepts and Architecture

Why JavaScriptCore Falls Short on Mobile

JavaScriptCore uses a multi-tier JIT compilation pipeline: first interpreting bytecode, then compiling hot functions through progressively optimizing compilers (Baseline, DFG, and FTL tiers in JSC's case). This approach is excellent for long-running server processes and web browsers where JavaScript executes for minutes or hours. On mobile, it's suboptimal for three reasons:

Cold startup: JSC must parse JavaScript source code, generate initial bytecode, and warm up its JIT tiers before achieving peak performance. This parsing and compilation happens every time the app launches, adding hundreds of milliseconds to startup time.

Memory overhead: JIT compilers need executable memory pages and maintain deoptimization metadata, profiling data, and compiled code caches. On memory-constrained mobile devices, this overhead directly impacts how many apps can remain in the background without being killed by the OS.

Binary size: JSC's full JIT compiler adds several megabytes to the application binary — a significant cost on mobile where download size directly affects install rates.

Hermes's Architecture: AOT Bytecode Compilation

Hermes takes a fundamentally different approach. Instead of compiling JavaScript at runtime, it pre-compiles source code to bytecode during the build phase using a tool called the Hermes compiler (hermesc). This bytecode is optimized and compact, designed to be directly executed by the Hermes virtual machine without runtime compilation.

The architecture consists of three components:

Hermes Compiler (hermesc): Takes JavaScript source code as input and produces optimized bytecode. This runs during your app's build process, not at runtime. The compiler performs constant folding, dead code elimination, and function-level optimizations.

Hermes VM: A register-based virtual machine that executes pre-compiled bytecode. Unlike JSC's stack-based VM, the register-based architecture produces more compact bytecode and requires fewer instructions for common operations.

Hermes Garbage Collector: A generational, non-moving garbage collector optimized for mobile. It avoids compaction pauses that can cause dropped frames and uses concurrent marking to minimize main-thread blocking.

Bytecode Format

Hermes bytecode (.hbc) is a compact binary format that encodes operations as variable-length instructions. A typical bytecode instruction is 2-4 bytes, compared to 8-16 bytes for JSC's internal representation. This compact encoding directly translates to smaller bundle sizes and faster parsing.

Compilation Pipeline

Architecture and Configuration

Enabling Hermes in React Native

For projects using React Native 0.70+, Hermes is the default engine. For older projects:

Android — In android/gradle.properties:

hermesEnabled=true

In android/app/build.gradle:

project.ext.react = [
    hermesCommand: "../../node_modules/react-native/sdks/hermesc/%OS-BIN%/hermesc",
    cliCommand: ["bundle", "--minify"],
]
 
apply from: "../../node_modules/react-native/react.gradle"

iOS — In ios/Podfile:

use_react_native!(
  :path => config[:reactNativePath],
  :hermes_enabled => true
)

Then run:

cd ios && pod install

Hermes Configuration Options

Fine-tune Hermes behavior through compiler flags:

// android/app/build.gradle
project.ext.react = [
    hermesFlags: [
        "-O",           // Enable optimizations
        "-output-source-map"  // Generate source maps for debugging
    ],
]

Bundle Size Impact

Hermes bytecode is significantly smaller than minified JavaScript:

SourceSizeFormat
Raw JS bundle~4.2 MBSource code
Minified JS~1.8 MBMinified source
Gzipped JS~450 KBCompressed source
Hermes bytecode~1.1 MBPre-compiled .hbc
Gzipped bytecode~320 KBCompressed bytecode

This size reduction comes from bytecode encoding being more compact than text-based JavaScript and from the compiler's dead code elimination removing unreachable code paths.

Step-by-Step Implementation

Verifying Hermes is Active

Add a diagnostic check to confirm Hermes is running:

// src/utils/hermesCheck.ts
export function isHermes(): boolean {
  return typeof HermesInternal === 'object' && HermesInternal !== null;
}
 
export function getHermesVersion(): string | null {
  if (isHermes()) {
    return HermesInternal?.getRuntimeProperties?.()?.['OSS Release Version'] ?? 'unknown';
  }
  return null;
}
 
// Log on app startup
console.log(`Engine: ${isHermes() ? 'Hermes' : 'JavaScriptCore'}`);
console.log(`Hermes Version: ${getHermesVersion() ?? 'N/A'}`);

Measuring Startup Performance

Quantify Hermes's startup improvement:

import { Performance } from 'react-native';
 
const startupTimestamps: Record<string, number> = {};
 
export function markStartup(label: string) {
  startupTimestamps[label] = performance.now();
}
 
export function logStartupMetrics() {
  const keys = Object.keys(startupTimestamps);
  for (let i = 1; i < keys.length; i++) {
    const current = keys[i];
    const previous = keys[i - 1];
    const delta = startupTimestamps[current] - startupTimestamps[previous];
    console.log(`[Startup] ${previous} → ${current}: ${delta.toFixed(1)}ms`);
  }
}
 
// Usage in App.tsx
markStartup('bundleLoaded');
// ... after navigation mount ...
markStartup('firstScreenRendered');
// ... after initial data load ...
markStartup('interactive');
logStartupMetrics();
 
// Typical results with Hermes:
// bundleLoaded → firstScreenRendered: 120ms
// firstScreenRendered → interactive: 80ms
// Total TTI: ~200ms (vs ~450ms with JSC)

Source Map Configuration for Debugging

Hermes requires specific source map configuration for accurate debugging:

# Generate Hermes-compatible source map
npx react-native bundle \
  --platform android \
  --dev false \
  --entry-file index.js \
  --bundle-output index.android.bundle \
  --sourcemap-output index.android.bundle.map \
  --hermes-flag "-O"
 
# Convert Hermes source map for Chrome DevTools
npx react-native-source-map \
  --input index.android.bundle.map \
  --output hermes-source-map.json

Mobile Performance Optimization

Real-World Use Cases

Use Case 1: E-Commerce App Cold Start Optimization

An e-commerce app with a complex home screen (product carousels, category grids, promotional banners) was experiencing 2.1-second cold starts with JavaScriptCore. After enabling Hermes:

// Track the improvement
const before = { engine: 'JSC', coldStart: 2100, warmStart: 800, memory: 145 };
const after = { engine: 'Hermes', coldStart: 1200, warmStart: 350, memory: 98 };
 
// Cold start: 43% faster
// Warm start: 56% faster
// Memory: 32% lower

The improvement came primarily from eliminating JavaScript parsing time. With JSC, the 1.8MB JavaScript bundle required ~800ms to parse on a mid-range Android device. Hermes bytecode required only ~50ms to load.

Use Case 2: Reducing Memory Pressure in Apps with Large State Trees

Apps with large Redux stores or complex state benefit significantly from Hermes's memory model:

// Before JSC: Large state tree consumed ~45MB heap
// After Hermes: Same state tree consumed ~28MB heap
 
interface AppState {
  products: Product[];       // ~10,000 items
  categories: Category[];   // ~500 items
  userHistory: History[];    // ~5,000 items
  cache: CacheMap;          // Variable size
}
 
// Hermes's compact object representation and generational GC
// handle this workload more efficiently than JSC's mark-and-sweep

Use Case 3: Debugging with Hermes Chrome DevTools

Hermes supports direct debugging through Chrome DevTools via the Chrome DevTools Protocol:

// Enable debugging in development
if (__DEV__) {
  // Hermes exposes the Chrome DevTools Protocol directly
  // Connect via chrome://inspect in Chrome browser
  
  // Set breakpoints in TypeScript/JSX source files
  // Step through async/await code
  // Inspect React component state and props
  
  console.log('Open chrome://inspect to debug Hermes');
}

Debugging workflow:

  1. Run your app with Metro bundler
  2. Open Chrome and navigate to chrome://inspect
  3. Click "inspect" under your device's Hermes target
  4. Use the Sources panel to set breakpoints in your source code
  5. Debug with full support for breakpoints, step-through, watches, and console evaluation

Best Practices for Production

  1. Always enable Hermes for release builds: Even if you use JSC in development for faster iteration, ensure Hermes is enabled for all production builds. The startup and memory improvements are too significant to ignore.

  2. Use source maps for production crash reporting: Generate Hermes-compatible source maps during your CI/CD build and upload them to your crash reporting service (Sentry, Bugsnag, etc.) for accurate stack traces.

  3. Profile memory usage with and without Hermes: Use Android Studio's Memory Profiler or Xcode Instruments to compare heap sizes. Document the improvement for stakeholders.

  4. Enable bytecode optimizations in production: Use the -O flag when compiling Hermes bytecode for release builds. This enables additional optimizations like constant folding and dead code elimination.

  5. Monitor GC pauses: Use Hermes's GC statistics API to monitor garbage collection frequency and duration. Unexpected spikes may indicate memory leaks in your JavaScript code.

  6. Keep Hermes updated: The Hermes engine receives continuous performance improvements. Each React Native release includes a newer Hermes version with optimizations for specific workloads.

  7. Test on low-end devices: Hermes's improvements are most pronounced on low-end devices where JIT compilation overhead is most expensive. Test on representative devices to validate real-world improvements.

  8. Use Hermes's sampling profiler: Hermes includes a built-in sampling profiler that can identify hot functions without adding instrumentation overhead to your code.

Common Pitfalls and Solutions

PitfallImpactSolution
Source maps not matching after Hermes compilationCrash reports point to wrong linesUse react-native-source-map to convert Hermes source maps to standard format
console.log statements slowing down HermesNoticeable performance penalty in release buildsUse babel-plugin-transform-remove-console to strip logs in production
Debugger breakpoints not workingCan't debug TypeScript sourceEnsure Metro is generating source maps and Chrome DevTools is loading them
Memory leak from uncleaned event listenersApp killed by OS after extended useUse useEffect cleanup functions and AbortController for async operations
Third-party library using JIT-specific V8 APIsRuntime errors with HermesReplace with Hermes-compatible alternatives; most popular libraries have been updated
Hermes bytecode larger than expectedApp size not improvingEnable -O optimization flag and check for dead code elimination

Performance Optimization

Using Hermes's Sampling Profiler

Hermes includes a built-in profiler that identifies CPU bottlenecks:

// Start profiling
global.HermesInternal?.enableSamplingProfiler?.(1000); // 1ms sampling interval
 
// ... run the code you want to profile ...
 
// Stop and dump profile
global.HermesInternal?.disableSamplingProfiler?.();
 
// The profile can be loaded in Chrome DevTools Performance tab

Optimizing Object Allocation Patterns

Hermes's generational GC favors objects that die young. Structure your code accordingly:

// Prefer: Temporary objects that are quickly discarded
function processItems(items: Item[]) {
  return items.map(item => ({
    ...item,
    price: calculatePrice(item),
    inStock: checkAvailability(item),
  }));
  // These intermediate objects are collected in young generation (fast)
}
 
// Avoid: Long-lived objects that survive multiple GC cycles
const globalCache: Map<string, ProcessedItem> = new Map();
// These objects get promoted to old generation and are more expensive to collect

Memory Monitoring

export function monitorMemory() {
  if (global.HermesInternal?.getHeapUsage) {
    setInterval(() => {
      const { totalSize, usedSize } = global.HermesInternal.getHeapUsage();
      const usagePercent = (usedSize / totalSize) * 100;
      
      if (usagePercent > 80) {
        console.warn(`[Memory] High usage: ${usagePercent.toFixed(1)}% ` +
                     `(${(usedSize / 1024 / 1024).toFixed(1)}MB)`);
      }
    }, 30000);
  }
}

Comparison with Alternatives

FeatureHermesJavaScriptCoreV8 (Chakra)QuickJS
Compilation StrategyAOT bytecodeJIT + interpreterJIT (multi-tier)Interpreter only
Startup TimeVery fast (~200ms)Slow (~500ms)Slow (~450ms)Fast (~250ms)
Steady-State PerformanceGoodVery good (JIT)Excellent (JIT)Moderate
Memory UsageVery lowHighHighVery low
Bundle SizeSmall (~1.1MB bytecode)Large (~4MB)Large (~5MB)Very small (~0.3MB)
DebuggingChrome DevToolsSafari WebInspectorChrome DevToolsLimited
React Native DefaultYes (since 0.70)LegacyNoNo
AOT CompilationStatic Hermes (upcoming)NoNoNo

Advanced Patterns and Techniques

Custom Hermes Bytecode Optimization

# Maximum optimization for release builds
hermesc -O -emit-binary -out output.hbc input.js
 
# Generate source maps alongside bytecode
hermesc -O -output-source-map -emit-binary -out output.hbc input.js
 
# Profile-guided optimization (future feature)
hermesc -O -profile-input=profile.json -out output.hbc input.js

Hermes Internal API for Runtime Inspection

interface HermesInternal {
  enableSamplingProfiler(interval: number): void;
  disableSamplingProfiler(): void;
  dumpSampledProfileToFile(filePath: string): void;
  getHeapUsage(): { totalSize: number; usedSize: number };
  getRuntimeProperties(): Record<string, string>;
}
 
// Guard all calls behind runtime checks
if (typeof HermesInternal !== 'undefined') {
  HermesInternal.enableSamplingProfiler(100);
}

Testing Strategies

describe('Hermes Compatibility', () => {
  it('should confirm Hermes is active in test environment', () => {
    // In CI, verify Hermes is being used
    if (process.env.CI) {
      expect(isHermes()).toBe(true);
    }
  });
 
  it('should handle Proxy objects correctly on Hermes', () => {
    const handler: ProxyHandler<object> = {
      get: (target, prop) => {
        if (prop in target) return target[prop as keyof typeof target];
        return undefined;
      },
    };
    const proxy = new Proxy({ name: 'test' }, handler);
    expect(proxy.name).toBe('test');
    expect(proxy.unknown).toBeUndefined();
  });
 
  it('should measure bytecode loading time', async () => {
    const start = performance.now();
    // Heavy module import triggers bytecode loading
    const module = await import('./heavyModule');
    const loadTime = performance.now() - start;
    expect(loadTime).toBeLessThan(100); // Should load bytecode in <100ms
    expect(module).toBeDefined();
  });
});

Hermes Memory Management and Garbage Collection

Hermes uses a generational garbage collector that separates short-lived objects (young generation) from long-lived objects (old generation). Young generation collections happen frequently and are fast because they only scan the small young generation space. Old generation collections happen less frequently but take longer because they scan the entire old generation. Understanding this model helps you write code that minimizes garbage collection pressure.

Avoid creating large numbers of temporary objects in hot code paths. String concatenation in loops, spread operators on large arrays, and object destructuring in frequently called functions all create temporary objects that increase young generation pressure. Use StringBuilder-style patterns for string building, mutate arrays in place when possible, and cache destructured values outside loops to reduce allocation frequency.

Monitor garbage collection activity using the Hermes runtime API or native profiling tools. Excessive GC pauses manifest as dropped frames during animations or delayed responses to user interactions. If GC is causing performance issues, consider moving large data structures to native modules that manage memory outside the JavaScript heap, or use TypedArray objects for numerical data that Hermes can manage more efficiently than regular arrays.

Future Outlook

Static Hermes represents the next evolution — true ahead-of-time compilation that generates optimized native machine code from JavaScript at build time. Early benchmarks show Static Hermes achieving 2-5x performance improvements for computational workloads and approaching C++ performance levels. Combined with Fabric's synchronous rendering, Static Hermes positions React Native as a genuinely high-performance mobile framework, eliminating the last major performance criticism.

Profiling Hermes Performance

Use the Hermes sampling profiler to identify performance bottlenecks in your React Native application. Enable the profiler in development builds and record a trace while performing the slow operation. The profiler outputs a JSON file that you can load into Chrome DevTools or the Hermes profiler visualization tool. Look for functions that appear frequently at the top of the call stack — these are the hot paths that consume the most execution time. Common bottlenecks include large list renders without virtualization, expensive computations in the render path, and frequent state updates that trigger unnecessary re-renders.

Conclusion

Hermes is the single most impactful performance optimization available to React Native developers. By shifting compilation from runtime to build time, it delivers dramatically faster startup, lower memory usage, and smaller application sizes — all through a simple configuration change.

Key takeaways:

  1. AOT bytecode compilation eliminates runtime parsing — 30-50% faster cold starts
  2. Compact bytecode reduces bundle size — typically 30-40% smaller than minified JS
  3. Generational GC minimizes memory pressure — 20-30% lower heap usage
  4. Chrome DevTools integration provides first-class debugging — breakpoints, profiling, and inspection
  5. Default in React Native 0.70+ — enable it today with zero code changes
  6. Static Hermes on the horizon — AOT native compilation promising near-C++ performance
  7. Most impactful on low-end devices — where JIT overhead is proportionally most expensive

Enable Hermes in your project today and measure the improvement. The combination of Hermes for fast startup and Fabric for smooth rendering makes React Native a genuinely high-performance mobile framework.