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 stringawait page.screenshot()– Returns binary PNG dataawait 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/snapshotRawmethods inobserve.tsregister themselves for lazy refresh viaregisterSnapshotForRefRefresh ensureRefMapForRefinref-state.tsenables 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →