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

> Discover how Ego-Browser's snapshot system architecture differs from Playwright. Learn about ref-aware layers, stable locators, and semantic snapshots.

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

---

**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`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/browser-runtime.ts))

The foundation is [`src/browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/browser-runtime.ts), which manages low-level CDP messaging:

```typescript
// 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`](https://github.com/citrolabs/ego-lite/blob/main/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.

```typescript
// 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`](https://github.com/citrolabs/ego-lite/blob/main/ref-state.ts) and [`ref-map.ts`](https://github.com/citrolabs/ego-lite/blob/main/ref-map.ts))

Ego-Browser's innovation is the **`RefMap`** class in [`src/ref-map.ts`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/helpers.ts))

The snapshot methods reach sandboxed scripts through [`src/helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/helpers.ts):

```typescript
// 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`](https://github.com/citrolabs/ego-lite/blob/main/format.ts))

The `help()` system in [`src/format.ts`](https://github.com/citrolabs/ego-lite/blob/main/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

```typescript
// 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

```typescript
// 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

```typescript
// 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`](https://github.com/citrolabs/ego-lite/blob/main/src/browser-runtime.ts) (CDP transport), [`src/driver/observe.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/observe.ts) (snapshot implementation), [`src/ref-state.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/ref-state.ts) (global ref management), [`src/ref-map.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/ref-map.ts) (ref storage), [`src/helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/helpers.ts) (API exposure), [`src/format.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/format.ts) (documentation)
- The `snapshot`/`snapshotRaw` methods in [`observe.ts`](https://github.com/citrolabs/ego-lite/blob/main/observe.ts) register themselves for lazy refresh via `registerSnapshotForRefRefresh`
- `ensureRefMapForRef` in [`ref-state.ts`](https://github.com/citrolabs/ego-lite/blob/main/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.