How Stable Are Ref Numbers (backendNodeId) Across ego-lite Snapshots?
Ref numbers (e.g., @23) in ego-lite are not stable across snapshots—they are short-lived identifiers tied to the Chrome DevTools Protocol (CDP) backend node IDs that change with every new snapshot.
When using ego-lite for browser automation, understanding the transient nature of ref numbers is critical for writing reliable scripts. These numeric shortcuts map to DOM elements via CDP backend node IDs, which are regenerated on every snapshot and cleared from memory when the page changes, navigates, or reloads.
What Are Ref Numbers in ego-lite?
In the ego-lite runtime, a ref is a shorthand notation (e.g., @12, @45) representing a backend node ID assigned by the Chrome DevTools Protocol when a snapshot is captured. According to the source code in src/format.ts, these @N syntax markers provide a concise way to reference elements within the immediate context of a single snapshot.
However, these identifiers are fundamentally volatile. The CDP backend can reuse or reassign node IDs after any DOM mutation, navigation, or page reload, meaning the numeric value assigned to a specific element in one snapshot carries no guarantee of referring to the same element—or any valid element—in subsequent snapshots.
Why Ref Numbers Are Not Persistent Across Snapshots
The instability of ref numbers stems from the architecture of the RefMap implementation in src/ref-map.ts. This module maintains an in-memory mapping between ref strings and backend node IDs, but it exhibits two critical behaviors that prevent persistence:
-
Complete regeneration: The
RefMapis rebuilt from scratch on every snapshot. TheRefMap.add…methods are only invoked during snapshot processing, meaning previous mappings are discarded entirely. -
Automatic clearing: When a new snapshot begins,
RefMap.clear()is called explicitly, wiping all previous ref-to-backendNodeId associations. This ensures that stale references cannot accidentally resolve to incorrect elements after the DOM has changed.
Because the underlying CDP protocol does not guarantee stable node IDs across debugging sessions, ego-lite treats each snapshot as an isolated context where refs are valid only for the duration of that specific capture.
How Ref Resolution Works in ego-lite
The runtime employs defensive logic in src/ref-state.ts to handle the ephemeral nature of refs. When a script attempts to use a ref, the ensureRefMapForRef() function performs a critical validation:
// Pseudo-code representation based on src/ref-state.ts
function ensureRefMapForRef() {
if (RefMap.isEmpty()) {
// Forces a fresh snapshot before resolving
triggerNewSnapshot();
}
}
This means that if you attempt to use a ref after the map has been cleared (due to navigation or a new snapshot), ego-lite will automatically capture a new snapshot before attempting resolution. However, this new snapshot generates entirely new ref numbers.
If a ref cannot be resolved in the current snapshot (because the element no longer exists or the ref number is stale), the resolver in src/element-resolver.ts throws an ElementResolutionError with an "Unknown ref" message.
Practical Example: Ref Lifetime Demonstration
The following example demonstrates how ref numbers become invalid across snapshots:
// Take a snapshot and capture a ref for the first button
const { snapshot } = await import('ego-browser');
const snap = await snapshot(); // Creates new RefMap
const btnRef = snap.refs.find(r => r.role === 'button')?.ref; // e.g., "@12"
// Using the ref within the same snapshot context works correctly
await click(btnRef); // Resolves via RefMap → backendNodeId
// After navigation, a new snapshot invalidates previous refs
await navigate('https://example.com'); // Triggers new snapshot, clears RefMap
await click(btnRef); // Throws "Unknown ref: 12"
In this scenario, the second click() call fails because the backend node ID associated with @12 no longer exists in the new CDP context, and the old ref mapping was destroyed when RefMap.clear() was called during the navigation-triggered snapshot.
Best Practices for Stable Element Selection
Since ref number stability cannot be guaranteed across ego-lite snapshots, use stable locators for long-running automation scripts:
// Preferred: Use CSS selectors or other stable locators
const locator = 'button[data-action="submit"]';
await click(locator); // Works reliably across any number of snapshots
// Avoid: Storing refs for reuse across navigation points
const savedRef = "@12";
await navigate('/new-page');
await click(savedRef); // Unpredictable: may resolve to wrong element or throw
Stable locators such as CSS selectors, XPath expressions, or text-based selectors remain valid regardless of how many snapshots are taken or how CDP reassigns internal node IDs.
Summary
- Ref numbers are transient: They represent CDP backend node IDs that change with every snapshot in ego-lite.
- RefMap is cleared unconditionally: The
RefMap.clear()method insrc/ref-map.tswipes all mappings when a new snapshot begins. - Automatic resnapshotting:
ensureRefMapForRef()insrc/ref-state.tsforces a fresh snapshot if refs are used after clearing, generating new numbers. - Error handling: Stale refs trigger
ElementResolutionErrorfromsrc/element-resolver.tsrather than resolving to incorrect elements. - Recommended approach: Use CSS selectors, XPath, or text locators instead of refs for automation that spans multiple snapshots or navigation events.
Frequently Asked Questions
Are ref numbers in ego-lite persistent across page navigations?
No, ref numbers are not persistent across navigations. When you navigate to a new page or trigger a new snapshot, the RefMap is cleared via RefMap.clear(), and the CDP backend generates entirely new backend node IDs. Any refs from previous snapshots become invalid and will throw an "Unknown ref" error if used.
What happens if I use a stale ref number in ego-lite?
If you attempt to use a ref after the RefMap has been cleared (either explicitly or through navigation), the ensureRefMapForRef() function in src/ref-state.ts detects that the map is empty and forces a fresh snapshot. However, the old ref number will not exist in the new snapshot, causing the element resolver in src/element-resolver.ts to throw an ElementResolutionError.
Can I rely on ref numbers for long-running automation scripts?
You should not rely on ref numbers for long-running scripts or tests that involve multiple snapshots, page reloads, or DOM mutations. According to the ego-lite source code in src/ref-map.ts and src/ref-state.ts, refs are designed for quick, single-snapshot interactions only. For durable element references, use stable locators like CSS selectors or XPath expressions.
Where does ego-lite store the mapping between refs and backend node IDs?
The mapping is stored in the RefMap class implemented in src/ref-map.ts. This in-memory structure maintains the relationship between @N ref strings and their corresponding CDP backend node IDs. However, this map is rebuilt from scratch during every snapshot processing cycle and is explicitly cleared when a new snapshot begins, making it a temporary storage mechanism rather than a persistent registry.
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 →