# How Lightpanda's Node Registry Functions Within the DOM Implementation

> Discover how Lightpanda's node registry assigns stable IDs to DOM nodes and creates bidirectional lookups for Zig pointers and Chrome DevTools Protocol integration.

- Repository: [Lightpanda/browser](https://github.com/lightpanda-io/browser)
- Tags: internals
- Published: 2026-03-14

---

**Lightpanda's node registry assigns stable numeric IDs to DOM nodes and maintains bidirectional hash-map lookups to bridge internal Zig pointers with the Chrome DevTools Protocol.**

The `lightpanda-io/browser` repository implements a headless browser engine in Zig that exposes its DOM to debugging tools via the Chrome DevTools Protocol (CDP). At the heart of this integration sits the **node registry**, a specialized mapping layer defined in `src/cdp/Node.zig` that translates between internal `webapi.Node` pointers and stable CDP node identifiers.

## Core Responsibilities of the Node Registry

### Assigning Stable Node IDs

Every DOM node exposed to CDP receives a unique, incrementing `node_id` through the `Registry.register` method. This method wraps the raw DOM pointer in a `Node` structure and stores it in two hash maps: `lookup_by_id` maps the numeric ID to the wrapper, while `lookup_by_node` maps the DOM pointer back to the same wrapper. This bidirectional mapping lives in `src/cdp/Node.zig` at lines 36-45.

### Preventing Duplicate Registrations

To ensure idempotency, the registry uses `lookup_by_node.getOrPut` to check if a node already exists before creating a new ID. If the DOM node was previously registered, the existing wrapper is returned, guaranteeing that the same pointer always resolves to the same CDP identifier across the session.

### Lifecycle Management and Cleanup

When a page navigates or the browser context tears down, `Registry.reset` clears both hash maps and recycles the internal arena and memory pool. This bulk cleanup operation, found at lines 63-68 of `src/cdp/Node.zig`, keeps subsequent navigation fresh without individual deallocations.

## Implementation Architecture

Each `BrowserContext` instantiated in `src/cdp/cdp.zig` maintains its own `Node.Registry` initialized via `Node.Registry.init(allocator)`. The registry persists for the lifetime of the context and provides the foundation for all DOM-related CDP commands.

The registry solves three critical engineering challenges:

- **Deterministic IDs**: CDP clients expect stable numeric references that persist across the page lifetime.
- **Performance**: Direct pointer-to-ID mapping eliminates the need to traverse the DOM tree for every request.
- **Memory Safety**: The registry owns a dedicated memory pool and arena, making node wrapper allocation cheap and enabling bulk reset without fragmentation.

## Practical Usage in CDP Domains

### Registering the Document Root

When handling `DOM.getDocument`, the domain handler registers the window's document node:

```zig
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
const page = bc.session.currentPage() orelse return error.PageNotLoaded;

// Register the document node – creates/looks‑up a wrapper with an id
const docNode = try bc.node_registry.register(page.window._document.asNode());

// Use the registry‑aware writer to serialise the node hierarchy
return cmd.sendResult(.{
    .root = bc.nodeWriter(docNode, .{ .depth = params.depth })
}, .{});

```

*Source*: `src/cdp/domains/dom.zig` lines 84-89

### Resolving Incoming Node IDs

Commands like `LP.getMarkdown` accept optional `nodeId` parameters that must resolve back to DOM pointers:

```zig
// LP.getMarkdown – optional nodeId parameter
const dom_node = if (params.nodeId) |nodeId|
    (bc.node_registry.lookup_by_id.get(nodeId) orelse return error.InvalidNodeId).dom
else
    page.document.asNode();

```

*Source*: `src/cdp/domains/lp.zig` lines 91-94

### Bulk Registration for Search Operations

The `DOM.performSearch` command utilizes the registry to convert node lists into ID lists:

```zig
// Inside Node.Search.List.create
for (nodes) |domNode| {
    const node = try registry.register(domNode);
    node_ids[i] = node.id;
}

```

*Source*: `src/cdp/Node.zig` lines 145-168

### Resetting on Navigation

The `BrowserContext.reset` method invokes the registry cleanup during page transitions:

```zig
pub fn reset(self: *Self) void {
    self.node_registry.reset();   // clears maps, arena, and pool
    self.node_search_list.reset();
}

```

*Source*: `src/cdp/cdp.zig` lines 87-90

## Summary

- The **node registry** in `src/cdp/Node.zig` maintains bidirectional mappings between DOM pointers and stable CDP node IDs.
- It uses two hash maps—`lookup_by_id` and `lookup_by_node`—to enable fast registration and resolution.
- The `register` method guarantees idempotency via `getOrPut`, preventing duplicate IDs for the same node.
- Each `BrowserContext` owns a fresh registry instance that gets reset during navigation to prevent ID leakage.
- CDP domains like `DOM` and `LP` rely on the registry to serialize node hierarchies and resolve client-provided node identifiers.

## Frequently Asked Questions

### What is the primary purpose of the node registry in Lightpanda?

The node registry serves as the translation layer between Lightpanda's internal Zig DOM pointers and the Chrome DevTools Protocol. It assigns stable numeric IDs to `webapi.Node` instances and maintains bidirectional lookups, allowing CDP clients to reference specific DOM elements without exposing internal memory addresses.

### How does the node registry prevent duplicate registrations?

When `Registry.register` is called, it uses `lookup_by_node.getOrPut` to check if the DOM pointer already exists in the registry. If found, it returns the existing wrapper with its assigned ID; otherwise, it creates a new entry. This ensures the same DOM node always maps to the same CDP identifier throughout the session.

### When is the node registry reset?

The registry resets whenever a `BrowserContext` navigates to a new page or shuts down. The `BrowserContext.reset` method calls `self.node_registry.reset()`, which clears both hash maps and recycles the internal arena and memory pool, ensuring fresh state for subsequent navigation.

### Which CDP domains interact with the node registry?

The `DOM` domain uses the registry to register document roots and element hierarchies in methods like `DOM.getDocument`. The `LP` (Lightpanda-specific) domain relies on it for commands such as `LP.getMarkdown` and `LP.getInteractiveElements` to resolve node IDs into DOM pointers for processing.