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.

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.
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.
The first step of tuning is measuring. Hermes exposes runtime statistics via getRuntimeProperties(). Some keys relevant to memory:
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.
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:
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.
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:
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:
hermes -Xgc-init-heap-size=8M -Xgc-max-heap-size=64M app.hbcThe 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.
A leak in JavaScript shows up as a heap that keeps rising even when the session is idle. How to hunt it:
A forgotten cleanup often looks like this:
function mount(ticker) {
const interval = setInterval(tick, 1000);
return () => clearInterval(interval);
}
const stop = mount(ticker);
stop(); // make sure this is called when the screen unmountsOnce 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.
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:
getRuntimeProperties() and observe heap trends, not absolute numbers.maxHeapSize is a safety net, not a target; match it to the app's natural size.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!