Understanding the @N Ref System (backendNodeId) in ego-browser and How RefMap Rebuilds on Snapshots
The @N ref system in ego-browser leverages Chrome DevTools Protocol backend node IDs to create stable DOM element references, automatically rebuilding the internal RefMap on each snapshot to ensure references stay synchronized with the current page state.
The citrolabs/ego-lite repository provides a lightweight browser automation framework that enables AI agents to interact with web pages using concise element addressing. This article explains how the @N ref system (backendNodeId) creates persistent handles to DOM elements and how the framework reconstructs its reference map when snapshots are taken.
What Is the @N Ref System?
The @N syntax represents a backend node ID—a unique identifier assigned by the Chrome DevTools Protocol (CDP) to every DOM node. In ego-browser, writing @42 tells the runtime to locate the element whose backendNodeId is 42 in the current CDP session.
These IDs are stable for the lifetime of the page, meaning @42 consistently points to the same node across multiple interactions. However, the IDs are session-specific; they change when the page reloads or when a fresh snapshot initializes a new CDP session. The runtime stores this mapping in a singleton browserRefMap (defined in package/ego-browser/src/ref-state.ts), which parses @N strings and resolves them to live DOM nodes.
How the RefMap Rebuilds on Snapshots
When the page state changes, the ref-map must synchronize with the new DOM structure. This happens through a four-stage pipeline:
1. Snapshot Request
Functions like snapshotRaw() in package/ego-browser/src/driver/observe.ts invoke the browser runtime's snapshot capability. This call returns a result object containing a refs array, where each entry includes the backendNodeId, accessibility role, and name for every node in the current DOM tree.
2. Conversion and Population
The helper browserSnapshotRefsToRefMap() (located in package/ego-browser/src/browser-runtime.ts) iterates over the refs array. For each entry, it populates the browserRefMap using RefMap.add(), associating the numeric ID with the node's metadata and current state.
3. Clearing Stale Data
Before inserting new entries, the runtime explicitly calls refMap.clear() (lines 9–11 in package/ego-browser/src/browser-runtime.ts). This prevents orphaned references from previous snapshots from polluting the current session.
4. Lazy Refresh Mechanism
If code attempts to resolve a selector like @123 while the map is empty, the ensureRefMapForRef() function in package/ego-browser/src/ref-state.ts automatically triggers a snapshot. This lazy initialization (lines 12–24) guarantees that refs are always resolvable without requiring manual snapshot calls.
Practical Code Examples
The ref system integrates seamlessly with helper functions and manual snapshot workflows:
// Resolve element center using @N notation
await helpers.elementCenter("@42");
// Internally calls ensureRefMapForRef then resolveElementCenter
// Manual snapshot inspection
const snap = await egoBrowser.snapshotRaw({ includeStableLocator: true });
console.log(snap.refs.map(r => `${r.backendNodeId}: ${r.role}`));
// RefMap is now populated for subsequent @N queries
// Force ref-map availability
await egoBrowser.ensureRefMapForRef("@99");
// Triggers snapshot only if map is empty
Summary
- @N syntax represents CDP
backendNodeIdvalues, providing stable element addresses for the page lifetime. - The RefMap rebuilds by clearing existing entries and repopulating from the
refsarray returned bysnapshotRaw(). - Stale data prevention occurs via
refMap.clear()inbrowser-runtime.tsbefore each snapshot ingestion. - Lazy loading via
ensureRefMapForRef()eliminates the need for manual snapshot management when using refs.
Frequently Asked Questions
What happens if I use an @N ref after the page reloads?
Backend node IDs are tied to a specific CDP session for a single page load. After a reload, the browser assigns new IDs to all DOM nodes, so previous refs like @42 become invalid and will fail to resolve until you capture a fresh snapshot.
How does ego-browser prevent stale refs from persisting between snapshots?
Before processing new snapshot data, the browserSnapshotRefsToRefMap() function in package/ego-browser/src/browser-runtime.ts calls refMap.clear() to wipe the existing map. This ensures only current DOM nodes from the latest refs array are addressable.
Can I use @N refs without explicitly calling snapshot methods?
Yes. The ensureRefMapForRef() function in package/ego-browser/src/ref-state.ts automatically triggers a snapshot if the RefMap is empty when you attempt to resolve an @N reference. This lazy initialization allows immediate use of ref-based selectors without manual state management.
Where does the backendNodeId originate?
The ID originates from Chrome DevTools Protocol's DOM domain. When snapshotRaw() executes in package/ego-browser/src/driver/observe.ts, it queries the browser for the current DOM tree, and each node's backendNodeId is captured in the refs array that ultimately populates the RefMap.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →