In a React Native app on Hermes, how does the garbage collector keep pauses off the JS thread, and when does React Native trigger a collection itself?
answer
- stop-the-world pauses freeze JS work
- Hades, mostly concurrent, since 0.65
- old generation marked in the background
- costs: write barriers, fragmentation
- severe onTrimMemory or iOS memory warning
basics
~20 sHermes's Hades collector does most old-generation marking and sweeping on a background thread, so the JS thread pauses only briefly, at some throughput cost. React Native also requests a collection on severe Android onTrimMemory levels and, since 0.81, on iOS memory warnings.
solid answer
~50 sA garbage collector that stops the world freezes the JS thread, so React updates, event handlers and JavaScript-driven animations wait for it. Hermes's collector, **Hades** (shipped in React Native 0.65), is **mostly concurrent**: it collects the short-lived young generation much as the older GenGC did, but marks and sweeps the old generation on a **background thread** while JavaScript keeps running; on 32-bit devices it runs as a single-threaded **incremental** collector instead. The price is throughput: costlier write barriers, free-list allocation and more heap fragmentation. React Native also asks the runtime to collect when the OS signals pressure: on Android only the severe `onTrimMemory` levels trigger it, the mild ones are logged and ignored, and since 0.81 an iOS memory warning triggers one too. A GC cannot free memory you still reference, so leaks remain your problem.
go deeper
Recall that Hermes has a garbage collector, that long GC pauses would freeze the JS thread, and that Hermes's collector is designed to keep them short.
Explain the generational split: young objects collected quickly, old generation marked and swept mostly in the background, and incremental on 32-bit devices.
Weigh the throughput costs against pause time, know which Android and iOS memory signals trigger a collection, and separate GC behaviour from leaks the app itself causes.
Judge where memory budgets should be enforced on low-end devices: in native modules that report external memory and in app code that releases references, since the collector's internals are largely outside an app team's control.
## Why garbage collection matters on the JS thread React Native runs your JavaScript on a single **JS thread** inside the Hermes runtime. Everything JavaScript does, from React rendering to event handlers to JavaScript-driven animation, shares that thread. **Garbage collection (GC)** is the engine reclaiming memory from objects nothing references any more. A simple collector does this **stop-the-world**: it halts JavaScript, walks the heap and resumes only when it has finished. On a phone, a long pause shows up as a frozen tap or a stuttering JavaScript animation. React Native's 2021 post on making Hermes the default describes why this became urgent: the New Architecture's renderer can call JavaScript synchronously from the UI thread, so a long GC pause on the JS side can now block user input directly. ## Hades: a mostly concurrent collector Hermes's earlier collector, **GenGC**, was single-threaded and generational: a **young generation** for new, short-lived objects collected by semi-space copying, and an **old generation** for survivors managed by mark-compact. Its weakness was long pauses on large apps. The replacement, **Hades**, shipped with React Native 0.65. Its design: - **Young generation**: collected the same way GenGC did it. These collections are short because most young objects are already dead. - **Old generation**: a **snapshot-at-the-beginning mark-sweep** collector that does most of its marking and sweeping on a **background thread** while JavaScript keeps executing. - **On 32-bit devices**: Hades runs as a **single-threaded incremental** collector, splitting the work into small slices rather than moving it to another thread. The React Native team reported p99.9 pause times of about 48 ms on 64-bit devices and around 88 ms on 32-bit devices, against GenGC averages of about 200 ms on a large Android app. Two terms from that description are worth being able to define: - **Snapshot-at-the-beginning**: the collector treats everything reachable when marking starts as live for that cycle. Objects that become unreachable during marking are simply collected in a later cycle, which keeps the concurrent marking correct without stopping JavaScript. - **Write barrier**: a small piece of work the engine performs on every pointer write while marking is in progress, so that an object JavaScript moves around cannot be missed by the background marker. ## What Hades trades away Concurrency is not free. The same post lists the costs: | Cost | Why it exists | |---|---| | More expensive **write barriers** | JavaScript mutates objects while the collector is marking, so each pointer write must inform the collector | | Slower **free-list allocation** | Mark-sweep leaves gaps, so allocation searches free lists instead of bumping a pointer | | More **heap fragmentation** | Swept memory is not compacted into one block | The design deliberately chose shorter pauses over raw throughput, and recovered memory through other optimisations. In an interview, naming the trade-off is what separates understanding from a slogan. ## When React Native asks for a collection The engine schedules collections on its own, but React Native also forwards operating-system memory pressure to the runtime: 1. **Android**: the platform reports memory pressure through `onTrimMemory` levels. React Native requests a GC only for the severe ones: `TRIM_MEMORY_RUNNING_CRITICAL`, `TRIM_MEMORY_BACKGROUND`, `TRIM_MEMORY_MODERATE` and `TRIM_MEMORY_COMPLETE`. The milder `TRIM_MEMORY_RUNNING_LOW`, `TRIM_MEMORY_RUNNING_MODERATE` and `TRIM_MEMORY_UI_HIDDEN` are logged and ignored. The collection is scheduled onto the JS runtime rather than run from the calling thread. 2. **iOS**: since React Native 0.81 a Hermes GC is triggered in response to the system's memory pressure warning. ## What GC cannot fix - **Reachable objects are not garbage.** A listener never removed, a growing module-level cache or a closure captured by a long-lived timer keeps memory alive, and no collector will free it. - **Native memory is invisible by default.** A small JavaScript object can own a large native buffer. JSI lets native code report that with `setExternalMemoryPressure`, so the collector weighs it when deciding when to run; code that does not report it can make the heap look smaller than the real footprint. - **Pauses still exist.** Hades shortens them; it does not remove them. A tight loop allocating millions of short-lived objects still costs JS-thread time. ## Summary Hermes keeps GC pauses short by doing the old generation's work concurrently (or incrementally on 32-bit devices), paying for it with write barriers, slower allocation and fragmentation, while React Native adds collections when Android reports severe memory pressure or iOS issues a memory warning.
- Why does Hades behave differently on 32-bit devices?On 32-bit devices Hades runs as a single-threaded incremental collector: instead of moving old-generation work to a background thread, it slices that work into small increments interleaved with JavaScript. Pauses are still bounded, though longer than on 64-bit, where the team reported about 88 ms versus 48 ms at p99.9.
- A native module hands JavaScript small objects that each hold a large image buffer, and memory climbs. How does the GC come into it?The collector sizes its decisions on the JavaScript heap, where each object looks tiny, so it may not run soon enough. JSI provides `setExternalMemoryPressure` so native code can attach the real external size to the object, letting the GC account for it. The objects must also become unreachable; reporting size does not free a leak.
Hades is a cleaning crew that tidies the back rooms while the shop stays open, instead of locking the doors to clean: customers barely wait, but the crew has to be told every time a shelf is moved, which costs a little extra work all day.
saying these in an interview costs you the question
- Hermes's GC stops JavaScript for the whole collection of the old generation
- Concurrent collection is cheaper than stop-the-world in every respect
- React Native runs a GC on every Android onTrimMemory callback
- A good garbage collector will free listeners you forgot to remove
- Hermes frees objects by reference counting the moment they die