Ego-Browser Snapshot System Architecture: How It Differs from Playwright

Ego-Browser's snapshot system is a stateful, ref-aware layer built on CDP that produces semantic snapshots with stable locators, while Playwright only returns raw HTML, PNG screenshots, or accessibility trees without built-in reference handling.

Playwright dominates browser automation, but ego-browser (citrolabs/ego-lite) introduces a fundamentally different approach to capturing page state. Its snapshot system goes far beyond page.content() or page.screenshot()—it generates structured, AI-friendly representations with auto-managed references that survive DOM changes. This article breaks down the architecture by examining the actual source code.

How Playwright Handles Snapshots

Playwright provides three separate methods with no cross-cutting state:

  • await page.content() – Returns raw HTML as a string
  • await page.screenshot() – Returns binary PNG data
  • await page.accessibility.snapshot() – Returns an accessibility tree object

Each call is independent and stateless. If you capture an element reference, you must resolve it immediately using CSS or XPath selectors. Playwright never regenerates a snapshot automatically—the user must explicitly call the method again.

Ego-Browser's Semantic Snapshot Architecture

Ego-Browser's snapshot system lives in package/ego-browser/src/driver/observe.ts and operates as a persistent, refreshable layer atop the Chrome DevTools Protocol (CDP). The architecture splits into five coordinated components.

1. CDP Transport Layer (browser-runtime.ts)

The foundation is src/browser-runtime.ts, which manages low-level CDP messaging:

// Core CDP methods exposed by the runtime
rawCdp(method: string, params?: object): Promise<any>
browserCdp(method: string, params?: object): Promise<any>

The ensureSession method guarantees a valid CDP session and automatically re-attaches if the connection drops. This resilience is essential because snapshot operations depend on stable backendNodeId values that are session-scoped.

2. Snapshot Entry Points (driver/observe.ts)

The public API surfaces two functions:

Function Returns Use Case
snapshot(options?) Promise<string> Human-readable text for AI agents
snapshotRaw(options?) Promise<{content, refs}> Full structured data with references

Both functions call the native ego.snapshot CDP method and update the ref map via browserSnapshotRefsToRefMap. Critically, registerSnapshotForRefRefresh registers these functions so the system can lazily re-snapshot when a stale ref is encountered.

// Get cleaned text (default for agents)
const text = await page.snapshot();
// → "Home\nLogin\n[Search]"

// Get full structure with refs and stable locators
const raw = await page.snapshotRaw({
  includeStableLocator: true,
  includeActionMarks: true
});

3. Ref Management (ref-state.ts and ref-map.ts)

Ego-Browser's innovation is the RefMap class in src/ref-map.ts. It stores:

  • Textual refs: @23, @42 (auto-generated per snapshot)
  • CDP backendNodeId: The stable Chrome node identifier
  • Role and name: Accessibility metadata
  • Stable locator: Optional loc= selector surviving DOM changes

The singleton browserRefMap lives in src/ref-state.ts alongside a lazy snapshot callback (snapshotImpl). When an operation like page.click("@23") executes, ensureRefMapForRef checks if the map is populated; if empty, it triggers a fresh snapshot automatically.

This is impossible in Playwright—there is no global ref state to validate or refresh.

4. Helper Exposure (helpers.ts)

The snapshot methods reach sandboxed scripts through src/helpers.ts:

// Exposed to the agent-visible context
snapshot: driverObserve.snapshot,
snapshotRaw: driverObserve.snapshotRaw

This allows AI agents running inside the browser harness to call page.snapshot() directly without escaping the sandbox.

5. API Documentation (format.ts)

The help() system in src/format.ts defines human-readable signatures and examples for page.snapshot and page.snapshotRaw, ensuring discoverability without leaving the REPL.

Key Architectural Differences

Capability Ego-Browser Playwright
Return type Semantic text or structured object with refs Raw HTML, binary image, or AX tree
Element references Auto-generated @N refs mapped to backendNodeId None—selectors resolved on-the-fly
Automatic refresh Yes, via ensureRefMapForRef No—manual re-invocation required
Stable locators Built-in includeStableLocator flag Manual CSS/XPath craftsmanship
Action history includeActionMarks inserts markers No equivalent
State management Global browserRefMap with lazy refresh Stateless per-call design

Practical Usage Patterns

Basic Agent Interaction

// Agent gets readable context
const context = await page.snapshot();

// Agent decides to click "Login"
await page.click("@23");  // Ref map auto-refreshes if needed

Robust Automation with Stable Locators

// Capture once with stable locators
const { refs } = await page.snapshotRaw({ includeStableLocator: true });

// Use loc= selector that survives DOM mutations
await page.fill("loc=input[name='search']", "ego-browser");
await page.click("loc=button:has-text('Go')");

Handling Stale Refs

// If @23 was from an old snapshot, this still works:
await page.click("@23");
// → ensureRefMapForRef detects empty map
// → triggers snapshotImpl() automatically
// → resolves @23 from fresh data

Performance and Design Trade-offs

Ego-Browser's snapshot system incurs memory overhead from the global browserRefMap and latency overhead from potential automatic re-snapshots. However, this trades favorably against:

  • Fragility of raw selectors: CSS paths break on dynamic UIs
  • Brittleness of coordinates: Pixel-based clicking fails on responsive layouts
  • API friction: Multiple Playwright calls (content(), screenshot(), accessibility.snapshot()) merge into one semantic operation

The closed-source ego runtime optimizes the native ego.snapshot CDP method, making the overhead acceptable for AI agent use cases where stability beats raw speed.

Summary

  • Ego-Browser's snapshot system is a stateful CDP layer producing semantic, ref-aware page representations with automatic refresh and stable locators
  • Playwright provides stateless, raw data (HTML/PNG/AX tree) without built-in reference management
  • Key source files: src/browser-runtime.ts (CDP transport), src/driver/observe.ts (snapshot implementation), src/ref-state.ts (global ref management), src/ref-map.ts (ref storage), src/helpers.ts (API exposure), src/format.ts (documentation)
  • The snapshot/snapshotRaw methods in observe.ts register themselves for lazy refresh via registerSnapshotForRefRefresh
  • ensureRefMapForRef in ref-state.ts enables self-healing automation by re-snapshotting when refs are missing

Frequently Asked Questions

What makes ego-browser's snapshot "semantic" compared to Playwright?

Ego-Browser returns structured data including element roles, names, and auto-generated references (@23) tied to Chrome's backendNodeId. Playwright's page.content() returns unprocessed HTML, and page.accessibility.snapshot() returns only the accessibility tree without stable cross-references. The refs array in snapshotRaw() lets agents reason about elements as persistent entities rather than volatile DOM paths.

How do stable locators work in ego-browser?

When includeStableLocator: true is passed to snapshotRaw(), each ref includes a loc= selector generated from accessibility properties (role, name, text content). These selectors use :has-text() and attribute patterns designed to survive minor DOM changes—unlike CSS paths that break when wrapper elements are added. This is implemented in the native ego.snapshot CDP method and surfaced through the selector field in ref objects.

Can Playwright emulate ego-browser's ref system?

Not natively. Playwright has no global ref map or automatic refresh mechanism. You could build a similar system by wrapping page.accessibility.snapshot() with custom ID generation and storage, but you would need to manually handle backendNodeId extraction, session persistence, and stale-ref detection. Ego-Browser's architecture bakes this into the runtime via browserRefMap and ensureRefMapForRef.

When should I use snapshot() versus snapshotRaw()?

Use snapshot() when feeding context to LLM agents that need concise, readable page state— it returns cleaned text only. Use snapshotRaw() when building robust automation that requires element references, stable locators, or action marks. The raw output includes the full refs array necessary for page.click("@N") operations and DOM-resilient scripting.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →