# How Ego-Browser Rebuilds the Ref Map on `snapshotRaw()` Calls

> Learn how Ego-Browser rebuilds the Ref Map on snapshotRaw() calls. It clears and repopulates refs ensuring reference IDs always resolve to current backend nodes.

- Repository: [CitroLabs/ego-lite](https://github.com/citrolabs/ego-lite)
- Tags: internals
- Published: 2026-08-29

---

**Ego-Browser clears its Ref Map before each snapshot and repopulates it with fresh `refs` from the returned snapshot data, ensuring reference IDs like `@12` always resolve to current backend nodes.**

The `snapshotRaw()` method in ego-browser is the foundation of agent-driven web automation. Each call triggers a coordinated rebuild of the internal **Ref Map**—the data structure that maps textual reference IDs to underlying browser backend nodes. This article breaks down the exact mechanism, with reference to the source code in `citrolabs/ego-lite`.

## The Three-Step Rebuild Process

When an agent invokes `snapshotRaw()`, ego-browser executes a precise sequence to maintain reference integrity:

### Step 1: Clear the Existing Ref Map

Before capturing new snapshot data, the runtime **empties the current map** to prevent stale references from persisting. This happens through a hook registered in [`ref-state.ts`](https://github.com/citrolabs/ego-lite/blob/main/ref-state.ts).

In [[`src/ref-state.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/ref-state.ts)](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/ref-state.ts), the registration system stores a refresh function:

```typescript
// ref-state.ts
let snapshotImpl: (() => Promise<unknown>) | null = null;

export function registerSnapshotForRefRefresh(fn: () => Promise<unknown>) {
  snapshotImpl = fn;
}

export async function refreshSnapshot() {
  if (!snapshotImpl) return;
  await snapshotImpl();
}

```

The actual clearing happens during this refresh phase, implemented in `snapshotImpl`.

### Step 2: Capture the Raw Snapshot

The `snapshotRaw()` function in [[`src/driver/observe.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/observe.ts)](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/driver/observe.ts) delegates to the embedded ego runtime:

```typescript
// driver/observe.ts – snapshotRaw implementation
export async function snapshotRaw(): Promise<{ content: string; refs: RefInfo[] }> {
  const result = await browserEgo().snapshot();
  return {
    content: result.content,
    refs: result.refs.map(r => ({
      id: r.id,
      backendNodeId: r.backendNodeId,
      role: r.role,
      name: r.name,
      nth: r.nth,
      frameId: r.frameId
    }))
  };
}

```

The response contains `content` (the serialized DOM) and `refs` (an array of reference metadata objects). Each ref includes `id`, `backendNodeId`, `role`, `name`, `nth`, and `frameId`.

### Step 3: Repopulate the Ref Map

After the raw snapshot resolves, ego-browser iterates through `result.refs` and rebuilds the mapping. This occurs in the `snapshotImpl` implementation within [`ref-state.ts`](https://github.com/citrolabs/ego-lite/blob/main/ref-state.ts):

```typescript
// ref-state.ts – snapshotImpl rebuilds the map
async function snapshotImpl() {
  refMap.clear();                    // Step 1: clear stale entries
  const raw = await snapshotRaw();   // Step 2: fetch fresh snapshot
  
  // Step 3: repopulate with new refs
  for (const r of raw.refs) {
    refMap.addWithFrame(
      r.id,
      r.backendNodeId,
      r.role,
      r.name,
      r.nth,
      r.frameId
    );
  }
}

```

The `refMap.addWithFrame()` method is defined in [`src/ref-map.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/ref-map.ts) and stores each reference with its frame context for cross-frame element resolution.

## Hook Registration Architecture

The connection between the observation layer and reference state management is established at module initialization in [`driver/observe.ts`](https://github.com/citrolabs/ego-lite/blob/main/driver/observe.ts):

```typescript
// driver/observe.ts – hook installation
registerSnapshotForRefRefresh(() => snapshotRaw());

```

This registration pattern decouples the snapshot-taking logic from the ref management logic, allowing [`ref-state.ts`](https://github.com/citrolabs/ego-lite/blob/main/ref-state.ts) to orchestrate the rebuild without hard-coding dependencies.

## Working Example: Snapshot and Reference Resolution

Here's how the rebuild process enables reliable element interaction:

```typescript
// Agent code
const page = await browser.newPage();

// Triggers full ref map rebuild
const raw = await page.snapshotRaw();
console.log(raw.refs);
// → [{ id: "12", backendNodeId: 45, role: "button", name: "Submit", nth: 0, frameId: 0 }]

// Subsequent click resolves via the fresh Ref Map
await page.click("@12");  // Maps "@12" → backendNodeId 45

```

Without the rebuild mechanism, stale `backendNodeId` values from previous snapshots would cause element resolution failures after DOM mutations.

## Key Source Files

| File | Responsibility |
|------|---------------|
| [`src/ref-map.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/ref-map.ts) | `RefMap` class with `clear()`, `addWithFrame()`, and lookup methods |
| [`src/ref-state.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/ref-state.ts) | Hook registration (`registerSnapshotForRefRefresh`), `snapshotImpl` orchestration |
| [`src/driver/observe.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/observe.ts) | `snapshotRaw()` implementation, hook installation |
| [`src/helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/helpers.ts) | Exports `snapshotRaw` to agent-facing API |

## Summary

- **Ref Map clearing** happens via `refMap.clear()` in the refresh hook before every snapshot
- **Fresh snapshot data** comes from `browserEgo().snapshot()` returning `{ content, refs }`
- **Map repopulation** uses `RefMap.addWithFrame()` to store each ref with complete metadata
- **Hook architecture** decouples [`driver/observe.ts`](https://github.com/citrolabs/ego-lite/blob/main/driver/observe.ts) from [`ref-state.ts`](https://github.com/citrolabs/ego-lite/blob/main/ref-state.ts) through `registerSnapshotForRefRefresh`

## Frequently Asked Questions

### What triggers the Ref Map rebuild in ego-browser?

Every call to `snapshotRaw()` triggers the rebuild through the registered refresh hook. The hook clears the map, fetches new snapshot data, and repopulates entries in sequence.

### Why does the Ref Map need to be cleared before each snapshot?

DOM mutations—such as element insertion, removal, or navigation—invalidate `backendNodeId` values. Clearing ensures no stale references persist that would cause `click()` or `fill()` operations to target wrong or non-existent nodes.

### What information does each ref entry contain?

Each ref includes: `id` (the `@N` string), `backendNodeId` (Chrome DevTools Protocol node ID), `role` (ARIA role), `name` (accessible name), `nth` (position among similar elements), and `frameId` (for cross-frame resolution).

### Can agents use `snapshotRaw()` directly or only through `snapshot()`?

Agents can call `snapshotRaw()` directly when they need the raw `{ content, refs }` structure. The standard `snapshot()` method wraps this with additional processing; both trigger identical Ref Map rebuild behavior.