You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[MCP] snapshot-mode=none still serializes the ARIA snapshot #41717
When MCP is configured with snapshot-mode=none, response construction still calls Tab.captureSnapshot(), which always runs page.ariaSnapshot(). The serialized snapshot is then discarded because the response does not include a Snapshot section.
On a large deterministic page, the current snapshotless response path measured about 166 ms. A narrow prototype that skips only the unused ARIA serialization measured about 4.6 ms, while preserving page headers, modal state, console links, download events, and output-budget cleanup.
With snapshot-mode=none, MCP should avoid generating an ARIA snapshot that will not be returned. It should continue to collect and emit all non-snapshot response state, including:
changed tab/page headers
modal state
console links
download events
output-budget housekeeping
Proposed change
Pass whether an ARIA snapshot is needed into Tab.captureSnapshot() and conditionally skip only the ariaSnapshot() call inside the existing modal-state race. Keep the rest of the response-building path unchanged.
I would like to work on this and can add focused coverage to the existing MCP snapshot-mode, console, dialogs, files, tabs, and output-size tests.
Hi, I'm the Playwright bot and I took a first look at this.
Confirmed by reading the source at tip-of-tree (npm 1.62.0-next, sha 9fb3602). With --snapshot-mode=none the response path still serializes an ARIA snapshot it never uses. Response._build() calls currentTabOrDie().captureSnapshot(...) unconditionally, and Tab.captureSnapshot() always runs page.ariaSnapshot({ mode: 'ai' }) inside the modal-state race. The resulting ariaSnapshot string is then discarded, because the Snapshot section is only emitted when _includeSnapshot !== 'none'. So the serialization is pure wasted work in this mode — the analysis in the report is accurate.
consttabSnapshot=this._context.currentTab()
? awaitthis._context.currentTabOrDie().captureSnapshot(...)// always called
: undefined;
...
if(tabSnapshot&&this._includeSnapshot!=='none'){// snapshot only USED here
...
addSection('Snapshot', ...);}
constmodalStates=awaitthis._raceAgainstModalStates(async()=>{constariaSnapshot=root
? awaitroot.ariaSnapshot({mode: 'ai', depth, boxes })
: awaitthis.page.ariaSnapshot({mode: 'ai', depth, boxes });// runs even for snapshot-mode=none
...
});
The snapshot object is still needed here for modalStates, consoleLink, and events, so captureSnapshot() can't simply be skipped — only the ariaSnapshot() call inside it is dead in this mode. Threading a "snapshot needed" flag into captureSnapshot() and skipping just that call, as proposed, looks like the right shape and preserves the modal-state race, headers, console links, download events, and output-budget cleanup.
This is a valid, well-scoped optimization. Existing coverage lives in tests/mcp/snapshot-mode.spec.ts (should respect --snapshot-mode=none), which is a good anchor for a regression test asserting the non-snapshot state is still emitted while no ARIA snapshot is produced.
I'm a first pass here, not the final word — I confirmed the wasted-work path by code inspection rather than benchmarking, so I can't vouch for the specific 166 ms → 4.6 ms numbers, but the redundant serialization is real.
What is the practical use case where you need --snapshot-mode=none and this optimization would make any meaningful difference? In general, this mode is an edge case which we would optimize for.
One clarification on the practical use case: this is not limited to sessions launched with --snapshot-mode=none. My repro uses --snapshot-mode=full, which is the default. Tools such as browser_tabs with action: "list" and browser_console_messages do not request a response snapshot, so _includeSnapshot remains none; today they still serialize and discard the full ARIA tree.
A normal agent loop may poll tab state or console output between actions and take an explicit snapshot only when it needs page semantics. On current main d448aac01, the median for one tab-list call plus one filtered console call was 172.763, 170.164, and 166.731 ms across three fresh sessions. Applying the small race-preserving change from #41719 reduced those medians to 4.663, 4.600, and 4.686 ms, about 97.26% by the median of medians.
The strict evaluator retained all 2,400 actionable nodes in automatic and explicit snapshots, checked that snapshots update after a DOM mutation, preserved console and modal state plus download events and files, and kept screenshot and error behavior unchanged. The focused Chromium MCP selection passed all 56 tests, along with build, TypeScript, ESLint, and diff checks.
I am happy to add or adjust focused tests, but I will wait for assignment or explicit community-contribution approval before opening a PR.
Dex Hünter (@dexhunter) Could you please share the page where you experience this issue? From our experience, 172ms is nothing when working with agents, because they spend time generating the tokens anyway. That said, 172ms is quite a lot for taking a snapshot, so we are interested in improving our snapshots, and would love to see a page where they are so slow.
Yes. I used a synthetic page so the reproduction is deterministic and shareable. It approximates an admin/workflow page with a large accessibility tree: 2,400 buttons, one input, one download link, 120 console messages, and one alert handler.
I start MCP with the default --snapshot-mode=full, navigate to the printed URL, then time one browser_tabs({ action: "list" }) call plus one browser_console_messages({ level: "error" }) call. Neither response includes a ### Snapshot section, but current main still builds the full ARIA snapshot internally.
I reran this as two reversed-order fresh pairs, with two warmups and ten measured calls per session. On current main 55679f5f3d4b4f3e2534ec0ce2fc5683ba2eaf3f, the combined baseline median was 342.144 ms. Preserving the existing modal race, console-link handling, event transfer, and cleanup while replacing only the unused ARIA payload with an empty string measured 16.104 ms (95.3% lower).
Environment: Node v24.13.1, macOS arm64, Google Chrome for Testing 151.0.7922.10.
Dex Hünter (@dexhunter) Is this representative of your actual page? If we optimize aria snapshot for this particular case, but it turns out your actual page has some very different structure, things might still be very slow for you.
Activity
github-actions commentedon Jul 9, 2026
Hi, I'm the Playwright bot and I took a first look at this.
Confirmed by reading the source at tip-of-tree (npm
1.62.0-next, sha9fb3602). With--snapshot-mode=nonethe response path still serializes an ARIA snapshot it never uses.Response._build()callscurrentTabOrDie().captureSnapshot(...)unconditionally, andTab.captureSnapshot()always runspage.ariaSnapshot({ mode: 'ai' })inside the modal-state race. The resultingariaSnapshotstring is then discarded, because theSnapshotsection is only emitted when_includeSnapshot !== 'none'. So the serialization is pure wasted work in this mode — the analysis in the report is accurate.The exact path
packages/playwright-core/src/tools/backend/response.ts—_build():packages/playwright-core/src/tools/backend/tab.ts—captureSnapshot():The snapshot object is still needed here for
modalStates,consoleLink, andevents, socaptureSnapshot()can't simply be skipped — only theariaSnapshot()call inside it is dead in this mode. Threading a "snapshot needed" flag intocaptureSnapshot()and skipping just that call, as proposed, looks like the right shape and preserves the modal-state race, headers, console links, download events, and output-budget cleanup.This is a valid, well-scoped optimization. Existing coverage lives in
tests/mcp/snapshot-mode.spec.ts(should respect --snapshot-mode=none), which is a good anchor for a regression test asserting the non-snapshot state is still emitted while no ARIA snapshot is produced.I'm a first pass here, not the final word — I confirmed the wasted-work path by code inspection rather than benchmarking, so I can't vouch for the specific 166 ms → 4.6 ms numbers, but the redundant serialization is real.
Triage run
yury-s commentedon Jul 9, 2026
What is the practical use case where you need
--snapshot-mode=noneand this optimization would make any meaningful difference? In general, this mode is an edge case which we would optimize for.luoshixin93-sudo commentedon Jul 10, 2026
dexhunter commentedon Jul 11, 2026
One clarification on the practical use case: this is not limited to sessions launched with
--snapshot-mode=none. My repro uses--snapshot-mode=full, which is the default. Tools such asbrowser_tabswithaction: "list"andbrowser_console_messagesdo not request a response snapshot, so_includeSnapshotremainsnone; today they still serialize and discard the full ARIA tree.A normal agent loop may poll tab state or console output between actions and take an explicit snapshot only when it needs page semantics. On current main
d448aac01, the median for one tab-list call plus one filtered console call was172.763,170.164, and166.731 msacross three fresh sessions. Applying the small race-preserving change from #41719 reduced those medians to4.663,4.600, and4.686 ms, about97.26%by the median of medians.The strict evaluator retained all 2,400 actionable nodes in automatic and explicit snapshots, checked that snapshots update after a DOM mutation, preserved console and modal state plus download events and files, and kept screenshot and error behavior unchanged. The focused Chromium MCP selection passed all 56 tests, along with build, TypeScript, ESLint, and diff checks.
I am happy to add or adjust focused tests, but I will wait for assignment or explicit community-contribution approval before opening a PR.
dgozman commentedon Jul 14, 2026
Dex Hünter (@dexhunter) Could you please share the page where you experience this issue? From our experience, 172ms is nothing when working with agents, because they spend time generating the tokens anyway. That said, 172ms is quite a lot for taking a snapshot, so we are interested in improving our snapshots, and would love to see a page where they are so slow.
dexhunter commentedon Jul 16, 2026
Yes. I used a synthetic page so the reproduction is deterministic and shareable. It approximates an admin/workflow page with a large accessibility tree: 2,400 buttons, one input, one download link, 120 console messages, and one alert handler.
I start MCP with the default
--snapshot-mode=full, navigate to the printed URL, then time onebrowser_tabs({ action: "list" })call plus onebrowser_console_messages({ level: "error" })call. Neither response includes a### Snapshotsection, but current main still builds the full ARIA snapshot internally.I reran this as two reversed-order fresh pairs, with two warmups and ten measured calls per session. On current main
55679f5f3d4b4f3e2534ec0ce2fc5683ba2eaf3f, the combined baseline median was342.144 ms. Preserving the existing modal race, console-link handling, event transfer, and cleanup while replacing only the unused ARIA payload with an empty string measured16.104 ms(95.3%lower).Environment: Node
v24.13.1, macOS arm64, Google Chrome for Testing151.0.7922.10.dgozman commentedon Jul 17, 2026
Dex Hünter (@dexhunter) Is this representative of your actual page? If we optimize aria snapshot for this particular case, but it turns out your actual page has some very different structure, things might still be very slow for you.
dgozman commentedon Jul 27, 2026
Closing as per above.