From 000541ddc0537ad2d100f98b319b4e79ecf3f600 Mon Sep 17 00:00:00 2001 From: Matthew Goodwin Date: Tue, 21 Jul 2026 16:10:31 -0500 Subject: [PATCH 1/2] perf(mcp): skip aria snapshot capture when the response discards it --- .../playwright-core/src/tools/backend/response.ts | 2 +- packages/playwright-core/src/tools/backend/tab.ts | 13 ++++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/playwright-core/src/tools/backend/response.ts b/packages/playwright-core/src/tools/backend/response.ts index d28de344964bf..6b83f6e7d7870 100644 --- a/packages/playwright-core/src/tools/backend/response.ts +++ b/packages/playwright-core/src/tools/backend/response.ts @@ -270,7 +270,7 @@ export class Response { addSection('Ran Playwright code', this._code, 'js'); // Render tab titles upon changes or when more than one tab. - const tabSnapshot = this._context.currentTab() ? await this._context.currentTabOrDie().captureSnapshot(this._includeSnapshotRoot, this._includeSnapshotDepth, this._includeSnapshotBoxes, this._clientWorkspace) : undefined; + const tabSnapshot = this._context.currentTab() ? await this._context.currentTabOrDie().captureSnapshot(this._includeSnapshotRoot, this._includeSnapshotDepth, this._includeSnapshotBoxes, this._clientWorkspace, this._includeSnapshot !== 'none') : undefined; const tabHeaders = await Promise.all(this._context.tabs().map(tab => tab.headerSnapshot())); if (this._includeSnapshot !== 'none' || tabHeaders.some(header => header.changed)) { if (tabHeaders.length !== 1) diff --git a/packages/playwright-core/src/tools/backend/tab.ts b/packages/playwright-core/src/tools/backend/tab.ts index 441f8a9f75b00..ea0b2e5b698f1 100644 --- a/packages/playwright-core/src/tools/backend/tab.ts +++ b/packages/playwright-core/src/tools/backend/tab.ts @@ -406,8 +406,19 @@ export class Tab extends EventEmitter { this._requests.length = 0; } - async captureSnapshot(root: playwright.Locator | undefined, depth: number | undefined, boxes: boolean | undefined, relativeTo: string | undefined): Promise { + async captureSnapshot(root: playwright.Locator | undefined, depth: number | undefined, boxes: boolean | undefined, relativeTo: string | undefined, includeAria: boolean = true): Promise { await this._initializedPromise; + // The caller will not render the aria snapshot, so skip the accessibility + // tree walk, which dominates response latency on heavy pages. Console, + // events and modal states are still reported. + if (!includeAria) { + if (this.modalStates().length) + return { ariaSnapshot: '', modalStates: this.modalStates(), events: [] }; + const tabSnapshot: TabSnapshot = { ariaSnapshot: '', modalStates: [], events: this._recentEventEntries }; + this._recentEventEntries = []; + tabSnapshot.consoleLink = await this._consoleLog.take(relativeTo); + return tabSnapshot; + } let tabSnapshot: TabSnapshot | undefined; const modalStates = await this._raceAgainstModalStates(async () => { const ariaSnapshot = root From 4da9ea10fade2a03341160773ed58bd6241dedfc Mon Sep 17 00:00:00 2001 From: Matthew Goodwin Date: Tue, 21 Jul 2026 23:27:46 -0500 Subject: [PATCH 2/2] chore(mcp): share the snapshot assembly tail between aria and skip paths --- .../playwright-core/src/tools/backend/tab.ts | 43 ++++++++++--------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/packages/playwright-core/src/tools/backend/tab.ts b/packages/playwright-core/src/tools/backend/tab.ts index ea0b2e5b698f1..cff0839a70103 100644 --- a/packages/playwright-core/src/tools/backend/tab.ts +++ b/packages/playwright-core/src/tools/backend/tab.ts @@ -408,28 +408,29 @@ export class Tab extends EventEmitter { async captureSnapshot(root: playwright.Locator | undefined, depth: number | undefined, boxes: boolean | undefined, relativeTo: string | undefined, includeAria: boolean = true): Promise { await this._initializedPromise; - // The caller will not render the aria snapshot, so skip the accessibility - // tree walk, which dominates response latency on heavy pages. Console, - // events and modal states are still reported. - if (!includeAria) { - if (this.modalStates().length) - return { ariaSnapshot: '', modalStates: this.modalStates(), events: [] }; - const tabSnapshot: TabSnapshot = { ariaSnapshot: '', modalStates: [], events: this._recentEventEntries }; - this._recentEventEntries = []; - tabSnapshot.consoleLink = await this._consoleLog.take(relativeTo); - return tabSnapshot; - } let tabSnapshot: TabSnapshot | undefined; - const modalStates = await this._raceAgainstModalStates(async () => { - const ariaSnapshot = root - ? await root.ariaSnapshot({ mode: 'ai', depth, boxes }) - : await this.page.ariaSnapshot({ mode: 'ai', depth, boxes }); - tabSnapshot = { - ariaSnapshot, - modalStates: [], - events: [], - }; - }); + let modalStates: ModalState[] = []; + if (includeAria) { + modalStates = await this._raceAgainstModalStates(async () => { + const ariaSnapshot = root + ? await root.ariaSnapshot({ mode: 'ai', depth, boxes }) + : await this.page.ariaSnapshot({ mode: 'ai', depth, boxes }); + tabSnapshot = { + ariaSnapshot, + modalStates: [], + events: [], + }; + }); + } else if (this.modalStates().length) { + // Matches the aria path's modal fallback below, without the race: there + // is no tree walk for a modal to interrupt. + modalStates = this.modalStates(); + } else { + // The caller will not render the aria snapshot, so skip the accessibility + // tree walk, which dominates response latency on heavy pages. Console and + // events are still reported via the shared tail below. + tabSnapshot = { ariaSnapshot: '', modalStates: [], events: [] }; + } if (tabSnapshot) { tabSnapshot.consoleLink = await this._consoleLog.take(relativeTo); tabSnapshot.events = this._recentEventEntries;