You can control how much memory Ultralight keeps and when it lets go, whether your UI runs on a memory-constrained device or shares RAM with a game engine.

The library reclaims memory on its own as pages come and go— a handful of settings and two calls decide how eagerly.

## Memory Profiles

`Config::memory_profile` sets how aggressively freed heap memory returns to the OS and balances allocation speed against memory footprint. The renderer reads this setting once when initialized, so configure it with the rest of your startup options in [Config](/docs/2.0/creating-the-renderer#content-configuring-the-library).

| Profile | Behavior |
|---|---|
| `MemoryProfile::Balanced` | The default profile. Freed memory returns to the OS at a moderate pace, and allocation favors speed. |
| `MemoryProfile::LowMemory` | Freed memory returns promptly, allocations are packed more densely, and garbage collection runs eagerly. Recommended for resource-constrained platforms and long-running pages— see [Embedded and Device UI](/docs/2.0/embedded-and-device-ui) for tuning memory-limited devices. |
| `MemoryProfile::Performance` | Freed memory is held for reuse and garbage collection runs less often, trading memory footprint for peak throughput on hosts with RAM to spare. |

## Caches and Heap Sizes

### Cache Limits

`Config::memory_cache_size` caps the library's cache of decoded images, compiled JavaScript, and similar resources (in bytes, 256 MiB by default). Increasing this limit can improve performance for resource-heavy applications at the cost of higher memory usage.

`Config::page_cache_size` sets how many pages the back-forward cache keeps in memory (`0` by default). If you increase this setting, you should raise `memory_cache_size` as well.

### Heap Target

`Config::max_heap_size` sets a soft target for the total JavaScript heap in bytes. This is not a hard limit— the heap can grow beyond this value, but the garbage collector then treats the heap as under memory pressure and reclaims more aggressively to bring it back toward the target. A value of `0` (the default) derives the target from `memory_profile`. You can find the remaining heap configuration options in the [Config](/api/cpp/2_0_0/structultralight_1_1_config.html) reference.

## Automatic Reclamation

Every `recycle_delay` seconds (`0.5` by default) the library automatically performs a lightweight recycle of its internal caches from inside `Renderer::Update()` and `Renderer::Render()`. Letting the engine run this timer is the recommended approach. Setting it to `0` disables the automatic recycler entirely for applications that call `Renderer::Recycle()` on their own schedule.

Garbage collection runs automatically as well. When `idle_gc_enabled` is active (`true` by default), the renderer collects the JavaScript heap after the application has been idle with no user input for `idle_gc_idle_time` seconds (`2.0` by default) and returns the freed memory to the OS.

While your application is actively rendering frames, collection still runs concurrently whenever the heap grows well past its previous collected size, triggering as eagerly as `memory_profile` specifies. You can set `idle_gc_enabled` to `false` if your application manages garbage collection directly.

> 🚧 Reclamation only happens while Update() is being pumped
>
> An application that stops calling `Renderer::Update()` while idle receives no automatic reclamation— call `Renderer::Recycle()` or `Renderer::PurgeMemory()` explicitly in that case. AppCore pumps this loop automatically (see [Updating and Rendering](/docs/2.0/updating-and-rendering) for integrating the renderer into your own frame update).

## Reclaiming on Your Schedule

You normally don't need to call `Renderer::Recycle()` because the automatic timer handles it— call it yourself only when you want to control when reclamation happens.

`RecycleMode::Lightweight` (the default) performs the same work as the automatic recycler and is cheap enough to call every frame. `RecycleMode::Full` is a thorough, heavier reclaim that can take noticeably longer on a busy page, making it best suited for idle periods or transitions like a loading screen.

`Renderer::PurgeMemory()` attempts to release as much memory as possible. Never call it from a callback or from driver code.

```cpp
#include <Ultralight/Ultralight.h>

using namespace ultralight;

RefPtr<Renderer> renderer;

void BeginLevelTransition() {
  // Pseudo-code, put the loading screen up here.
  ShowLoadingScreen();

  ///
  /// Nobody's watching the UI, so do a thorough reclaim now instead
  /// of paying for it mid-gameplay.
  ///
  renderer->Recycle(RecycleMode::Full);
}
```

## Measuring Memory Usage

`Renderer::GetMemoryUsage()` returns formatted memory statistics as a string you can display in a debug overlay, while `Renderer::LogMemoryUsage()` outputs the same information to your logger.

To inspect metrics over time, the Profiler periodically samples memory usage, VRAM consumption, and cache sizes— see [Profiling and Tracing](/docs/2.0/profiling-and-tracing) for tracking counters in your profiling tools.

## Controlling the Collector

### The Garbage Collection Listener

In the Pro edition and higher, you can observe and control the JavaScript heap's foreground collections— the collections that briefly pause the thread pumping `Renderer::Update()`. You install a [`GCListener`](/api/cpp/2_0_0/classultralight_1_1_g_c_listener.html) using `Renderer::set_gc_listener()`, guarded by `UL_HAS(GC_LISTENER)` (see [Editions and Feature Macros](/docs/2.0/editions-and-feature-macros) for edition flags). Your application retains ownership of the listener, which must outlive the `Renderer` or be cleared with `nullptr` before destruction, and all callbacks fire on the thread that pumps `Renderer::Update()`.

`OnRequestGC()` fires when the engine decides the heap is worth collecting. By default, the collection runs immediately through `Renderer::CollectNow()` with the recommended code-drop mode. If you override `OnRequestGC()`, your application owns the collection— you must call `CollectNow()` with the mode you were given, either immediately or deferred to your run loop on the same thread to hide the pause. Deferring moves the pause rather than eliminating it, and the collection never runs if you do not call `CollectNow()`.

`OnPressureChanged()` fires when JavaScript heap pressure rises past an internal threshold, even with automatic collection disabled. `OnCollectComplete()` fires whenever a foreground collection finishes, reporting how long the collection paused the thread and how many bytes were reclaimed.

### Explicit Collection and Heap Status

`Renderer::CollectNow()` reclaims only the JavaScript heap while leaving rendering and GPU caches intact, so the next frame pays no re-warm cost. It runs synchronously by default, but can start a background collection instead. If you call it while JavaScript is currently executing on the stack, the collection begins once the script returns.

`Renderer::gc_status()` returns a snapshot of current heap pressure, elapsed time since the last collection, and live heap bytes that is cheap enough to poll every frame. When calling `Renderer::Recycle()`, you can supply a `CodeDropMode` alongside `RecycleMode::Full` to choose whether to discard `Nothing`, `Linked` JIT code, or `All` compiled code, balancing memory savings against re-warm cost (the code parameter has no effect with `RecycleMode::Lightweight`).

```cpp
#include <Ultralight/Ultralight.h>

using namespace ultralight;

RefPtr<Renderer> renderer;

#if UL_HAS(GC_LISTENER)
class MyGCListener : public GCListener {
 public:
  ///
  /// The engine wants to collect. Remember the request instead of
  /// running it now, so the pause lands on our next loading screen.
  ///
  void OnRequestGC(Renderer* caller, const GCInfo& info,
                   CodeDropMode mode) override {
    pending_renderer_ = caller;
    pending_mode_ = mode;
  }

  ///
  /// Call this from the loading screen, on the thread that pumps
  /// Renderer::Update().
  ///
  void RunDeferredGC() {
    if (pending_renderer_) {
      pending_renderer_->CollectNow(pending_mode_);
      pending_renderer_ = nullptr;
    }
  }

 private:
  Renderer* pending_renderer_ = nullptr;
  CodeDropMode pending_mode_ = CodeDropMode::Nothing;
};

MyGCListener gc_listener;

void AttachGCListener() {
  ///
  /// The Renderer doesn't take ownership, so our listener must outlive
  /// it (or be detached with nullptr before the Renderer is destroyed).
  ///
  renderer->set_gc_listener(&gc_listener);
}
#endif
```
