# How PageCollector Manages Multiple Browser Pages and Selection in Puppeteer

> Discover how PageCollector simplifies managing multiple browser pages and selections in Puppeteer. Learn about its WeakMap storage, event tracking, and explicit page selection APIs.

- Repository: [ChromeDevTools/chrome-devtools-mcp](https://github.com/chromedevtools/chrome-devtools-mcp)
- Tags: how-to-guide
- Published: 2026-02-16

---

**PageCollector uses a WeakMap-based storage system to isolate per-page data buckets, automatically tracks browser targets via Puppeteer events, and provides explicit page-selection APIs for retrieving collected resources.**

The `PageCollector` class in the `ChromeDevTools/chrome-devtools-mcp` repository serves as a generic abstraction for watching a Puppeteer `Browser` instance and maintaining independent collections of resources—such as network requests, console messages, and audit issues—for every `Page` it discovers. Understanding how this class manages multiple browser pages and selection is essential for building reliable browser automation tools that need to isolate data per tab.

## Per-Page Storage Architecture

At the core of `PageCollector` is a private `WeakMap` that ensures data isolation between browser pages while allowing automatic garbage collection when pages close.

The storage is defined in [`src/PageCollector.ts`](https://github.com/ChromeDevTools/chrome-devtools-mcp/blob/main/src/PageCollector.ts) as:

```typescript
protected storage = new WeakMap<Page, Array<Array<WithSymbolId<T>>>>();

```

This structure maps each Puppeteer `Page` to an array of **navigation buckets**. Each bucket is itself an array containing collected items, where every item is stamped with a stable numeric ID using the `stableIdSymbol` property. The newest navigation always occupies index `0`, with older navigations following up to the configurable limit `#maxNavigationSaved` (defaulting to `3`).

## Automatic Page Discovery

Rather than requiring manual registration for every new tab, `PageCollector` hooks into Puppeteer's browser-level events to automatically discover and track pages.

After calling `init(pages)` with an initial set of pages, the collector subscribes to the browser's lifecycle events:

```typescript
this.#browser.on('targetcreated', this.#onTargetCreated);
this.#browser.on('targetdestroyed', this.#onTargetDestroyed);

```

The `targetcreated` handler obtains the new `Page` instance via `await target.page()` and invokes `addPage`, while `targetdestroyed` triggers `cleanupPageDestroyed` to detach listeners and remove the page's entry from the `WeakMap`. Both handlers include `try/catch` blocks and route errors through the shared logger.

## Initializing and Tracking Pages

When `addPage(page)` is called, it delegates to the private `#initializePage(page)` method. If the page already exists in `storage`, the call returns immediately to prevent duplicate tracking.

For new pages, the initialization process:

1. Creates a fresh ID generator using `createIdGenerator()`
2. Inserts an empty navigation bucket (`[[]]`) into the `WeakMap`
3. Invokes the user-provided `#listenersInitializer` callback, which attaches event listeners; each collected resource receives a generated ID and is pushed into the current navigation bucket (`navigations[0]`)

```typescript
const idGenerator = createIdGenerator();
const storedLists: Array<Array<WithSymbolId<T>>> = [[]];
this.storage.set(page, storedLists);

```

## Handling Navigation Buckets

To maintain data isolation across page navigations, `PageCollector` splits storage buckets whenever the main frame navigates. The listener attached during initialization monitors the `framenavigated` event:

```typescript
listeners['framenavigated'] = (frame: Frame) => {
  if (frame !== page.mainFrame()) return;
  this.splitAfterNavigation(page);
};

```

The `splitAfterNavigation` method prepends a fresh empty bucket to the navigation array and enforces the retention limit:

```typescript
navigations.unshift([]);
navigations.splice(this.#maxNavigationSaved);

```

This ensures that only the most recent three navigations (by default) are retained in memory, preventing unbounded growth during long-running sessions.

## Selecting Data for Specific Pages

All public query methods require an explicit `page` argument, ensuring callers explicitly specify which tab's data they want to retrieve:

- **`getData(page, includePreservedData?)`**: Returns items from the current navigation bucket (`navigations[0]`). When `includePreservedData` is `true`, it concatenates all saved navigation buckets.
- **`getIdForResource(resource)`**: Extracts the stable ID previously attached to a resource.
- **`getById(page, stableId)`**: Iterates through all buckets for the specified page and returns the matching resource, throwing if not found.

Because `storage` is a `WeakMap`, entries for closed pages are automatically eligible for garbage collection once `cleanupPageDestroyed` removes the reference.

## Specialized Collectors

`ConsoleCollector` and `NetworkCollector` extend `PageCollector` while reusing the same per-page storage mechanics. They inject additional listeners for Chrome DevTools Protocol (CDP) events such as `Audits.issueAdded` and `Runtime.exceptionThrown`.

`ConsoleCollector` additionally registers a `PageEventSubscriber` to aggregate duplicate issues per navigation. `NetworkCollector` overrides `splitAfterNavigation` to implement custom logic that retains only requests belonging to the most recent navigation, discarding earlier ones to optimize memory usage.

## Practical Code Examples

### Initialize a collector for all current pages

```typescript
import {Browser} from 'puppeteer-core';
import {PageCollector} from 'chrome-devtools-mcp/src/PageCollector.js';

// `browser` is a launched Puppeteer Browser
const collector = new PageCollector(browser, collect => ({
  request: req => collect(req),
}));

await collector.init(await browser.pages());

// Later, when a new tab opens automatically it will be added.

```

### Retrieve resources for a specific page

```typescript
const pages = await browser.pages();
const page = pages[0];               // pick the page you care about
const requests = collector.getData(page); // only items from the current navigation

```

### Keep data from the last three navigations

```typescript
// Pass `true` to include preserved buckets
const allRecent = collector.getData(page, true);

```

### Use the NetworkCollector (navigation-aware)

```typescript
import {NetworkCollector} from 'chrome-devtools-mcp/src/PageCollector.js';

const netCollector = new NetworkCollector(browser);
await netCollector.init(await browser.pages());

page.emit('request', request);          // collected
page.emit('framenavigated', page.mainFrame()); // splits navigation

```

### Access a resource by its stable ID

```typescript
const first = collector.getData(page)[0];
const id = collector.getIdForResource(first);
const same = collector.getById(page, id); // returns `first`

```

## Summary

- **PageCollector** maintains isolated data per browser page using a `WeakMap<Page, Array<Array<WithSymbolId<T>>>>` structure stored in [`src/PageCollector.ts`](https://github.com/ChromeDevTools/chrome-devtools-mcp/blob/main/src/PageCollector.ts).
- **Automatic discovery** occurs via Puppeteer's `targetcreated` and `targetdestroyed` events, ensuring new tabs are tracked without manual registration.
- **Navigation buckets** separate data by page navigation, retaining a configurable number of historical navigations (default 3) to prevent memory leaks.
- **Explicit page selection** is required for all queries via methods like `getData(page)` and `getById(page, stableId)`, ensuring callers specify exactly which tab's data to retrieve.
- **Specialized implementations** such as `ConsoleCollector` and `NetworkCollector` extend the base class while overriding navigation handling for domain-specific optimizations.

## Frequently Asked Questions

### How does PageCollector prevent memory leaks when browser pages close?

PageCollector uses a `WeakMap` for its internal `storage` property, which does not prevent garbage collection of its keys. When a page closes, the `targetdestroyed` event triggers `cleanupPageDestroyed`, which removes the page's entry from the map. Once the reference is removed, the `WeakMap` entry becomes eligible for garbage collection, preventing memory accumulation in long-running browser sessions.

### What is the difference between getData(page) and getData(page, true)?

The `getData(page)` method returns only the items collected during the current navigation bucket (index 0), representing the most recent page load. When called with `getData(page, true)`, the method concatenates all preserved navigation buckets—up to the `#maxNavigationSaved` limit (default 3)—allowing access to historical data from previous navigations within the same tab.

### How does NetworkCollector customize navigation handling compared to the base PageCollector?

While the base `PageCollector` retains all collected items across navigations (up to the maximum limit), `NetworkCollector` overrides the `splitAfterNavigation` method to implement domain-specific cleanup. Specifically, it retains only network requests that belong to the most recent navigation, actively discarding earlier requests to optimize memory usage for high-traffic network monitoring scenarios.

### Can PageCollector track pages that open after initialization?

Yes, PageCollector automatically tracks pages that open after initialization through its subscription to Puppeteer's browser-level events. After calling `init(pages)` with the initial set of pages, the collector listens for `targetcreated` events on the browser instance. When a new target (such as a new tab) is created, the handler obtains the Page object and registers it via `addPage`, ensuring seamless tracking of dynamically opened pages without manual intervention.