Learn Hermes JS Engine - Advanced Memory & GC Tuning
Episode 17 of 23

Learn Hermes JS Engine - Advanced Memory & GC Tuning

Diving into advanced Hermes memory settings: benchmarking heap behavior and GC pause time, GC configuration for low-memory devices, plus profiling memory leaks and fragmentation.

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

Introduction

Episode 16 opened the compiler's hood. Episode 17 opens the memory pipes. The Hermes GC works behind the scenes: collecting garbage, compacting the heap, and occasionally pausing execution for a moment. For apps running on devices with tight RAM, a different GC configuration can mean the difference between life and death in terms of performance.

This episode's roadmap: benchmarking heap behavior and GC pause time, GC configuration for low-memory devices, then profiling memory leaks and fragmentation.

Hermes Memory Model and GC

Remember the foundations from episode 6: Hermes uses a generational GC that separates young and old objects, the heap is managed in arenas, and compaction unifies fragmented memory. All those mechanisms are controlled by one configuration object: GCConfig.

It's also important to remember Hermes' JIT-free nature: no memory is allocated for runtime-generated code, so the memory profile is more stable and predictable. This advantage is what makes Hermes GC tuning feel clean — there are fewer variables to control.

Benchmarking Heap Behavior and GC Pauses

The first step of tuning is measuring. Hermes exposes runtime statistics via getRuntimeProperties(). Some keys relevant to memory:

JSbaca-statistik-gc.js
const p = HermesInternal.getRuntimeProperties();
console.log("Heap size", p["kGCHeapSize"]);
console.log("Total pause", p["kGCTotalPauseTime"]);

Read these values periodically during a benchmark scenario — for example long scrolling or rendering a large list — and record the worst numbers. In Chrome DevTools, as in episode 7, a heap snapshot also shows the object distribution. Together, the two show whether the heap is swelling, whether GC pauses are felt on the UI thread, and at which point the problem occurs.

Make measurement a routine: record a baseline before changing any configuration. Without a baseline, every GC change can only be judged by feeling, not by objective numbers.

Warning

Don't compare absolute numbers across devices. What matters is the trend: whether the heap keeps rising throughout a session, which indicates a leak, or stabilizes at one level, which means healthy.

Measuring and Suppressing GC Pause Time

GC pause is the time JavaScript execution is stopped for collection work. In mobile apps, a long pause shows up as jank — frames feel stuttered. Strategies to suppress it:

  • Reduce allocation: every discarded object is GC work; the reuse patterns from episode 15 apply directly.
  • Keep the heap in bounds: a smaller heap collects faster, but GC frequency rises. That's a trade-off to measure.
  • Avoid momentary peaks: GC triggers are often sudden allocations; smooth out memory-hungry work.

To learn the actual pauses, profile allocation cycles in DevTools and compare them with frame time. If pauses start dominating, it's time to consider a tighter heap limit.

One way to remove noise: run the same benchmark repeatedly and take the median, not the average. Averages are easily pulled up by a single outlier spike that isn't from your code.

GC Configuration for Low-Memory Devices

For entry-level devices, cap the heap so the app never forces the OS into page eviction. At the C++ embedding level, configuration uses GCConfig and RuntimeConfig:

Linuxgc-config.cpp
auto gc = hermes::vm::GCConfig::Builder()
    .withInitHeapSize(1 << 20)
    .withMinHeapSize(1 << 20)
    .withMaxHeapSize(1 << 24)
    .build();
 
auto runtimeConfig = hermes::vm::RuntimeConfig::Builder()
    .withGCConfig(gc)
    .build();
 
auto runtime = HermesRuntime::make(runtimeConfig);

In a standalone binary or while debugging, the same limits can be applied via command-line flags:

gc-flags-cli.sh
hermes -Xgc-init-heap-size=8M -Xgc-max-heap-size=64M app.hbc

The rule of thumb: maxHeapSize is a safety net, not a target. If your app frequently touches the limit, the GC is forced to work harder. Reduce allocation on the code side, then adjust the heap limit to follow the app's natural size — not the other way around.

Start with a small heap, then raise it little by little until the app stabilizes. The starting numbers are just guesses until measured; let the data decide the final limit.

Profiling Memory Leaks and Fragmentation

A leak in JavaScript shows up as a heap that keeps rising even when the session is idle. How to hunt it:

  • Take a heap snapshot before and after a repeated scenario, for example open-close screens or start-stop tasks.
  • Compare object retainers in both snapshots; objects that stay alive even though they're no longer used are the prime suspects.
  • Watch out for event listeners and closures that are never released — both are classic leak sources in React Native apps.

A forgotten cleanup often looks like this:

JSlepas-listener.js
function mount(ticker) {
  const interval = setInterval(tick, 1000);
  return () => clearInterval(interval);
}
 
const stop = mount(ticker);
stop(); // make sure this is called when the screen unmounts

Once a leak is identified, the fix is consistent: make sure cleanup is called on every exit path, not just the main scenario. Closures held by an event emitter are a common example that slips past code review.

Fragmentation is another story: a heap that's empty but scattered so large allocations fail. The Hermes GC handles compaction periodically, so the indicator is an allocation that's refused even though total memory remains. In that case, check whether a giant object is being allocated repeatedly, for example a large buffer or canvas, and consider reusing it.

Conclusion

Hermes memory can be controlled: measure with runtime statistics and heap snapshots, control GC pauses by reducing allocation, cap the heap for low-memory devices, and hunt leaks and fragmentation by comparing retainers across snapshots. Good tuning always starts from data, not guesses.

The essentials to take home:

  • Read GC statistics via getRuntimeProperties() and observe heap trends, not absolute numbers.
  • GC pauses are felt as jank; reduce allocation and smooth out peaks to suppress it.
  • maxHeapSize is a safety net, not a target; match it to the app's natural size.
  • Leaks are hunted by comparing heap snapshots and retainers before-after a scenario.
  • Fragmentation is detected by large allocations that fail even though total memory is available.

In episode 18 we close Phase 5: Debugging Native Integration — debugging the JS-native bridge in React Native, seeing internal calls and the native module boundary, and handling crash reporting and native stack traces. See you there!

Learn Hermes JS Engine - Advanced Memory & GC Tuning | Learn Hermes JS Engine