How PageCollector Manages Multiple Browser Pages and Selection in Puppeteer
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 as:
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:
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:
- Creates a fresh ID generator using
createIdGenerator() - Inserts an empty navigation bucket (
[[]]) into theWeakMap - Invokes the user-provided
#listenersInitializercallback, which attaches event listeners; each collected resource receives a generated ID and is pushed into the current navigation bucket (navigations[0])
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:
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:
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]). WhenincludePreservedDataistrue, 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
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
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
// Pass `true` to include preserved buckets
const allRecent = collector.getData(page, true);
Use the NetworkCollector (navigation-aware)
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
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 insrc/PageCollector.ts. - Automatic discovery occurs via Puppeteer's
targetcreatedandtargetdestroyedevents, 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)andgetById(page, stableId), ensuring callers specify exactly which tab's data to retrieve. - Specialized implementations such as
ConsoleCollectorandNetworkCollectorextend 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.
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 →