我最近在做一个内部小工具,把浏览器自动化能力接进 MCP,起因很简单:团队想让我测一下,同一条指令在不同执行方式下,到底要多长时间才能拿到结果。跑下来之后发现,数据比预想的有意思得多。比如同样是“打开页面读标题”,让 MCP 去启动全新浏览器进程,和连上一个已经开着的 Chrome 实例,耗时能差出好几倍。这个差距在单次手动操刀时无所谓,但一旦 LLM 调用 MCP 工具、要走多轮交互,就会明显感知到“卡顿”还是“顺滑”。这篇我把自己做的 Playwright MCP 性能基准测试完整摊开来讲,覆盖测试环境、场景设计、三种浏览器执行方式的响应时间对比,以及我实际踩过的几个坑,给后面想做同类评测或直接做工具优化的朋友一份能照着搬的参考。
1. 测试环境与基线搭建
做性能测试最忌讳上来就乱测。同一个 MCP 工具,跑在 GUI 模式和 headless 模式下,响应时间会差很多;用的是官方默认 Playwright 内核还是系统里已有的 Chrome,差异也不是一点半点。所以第一步先明确我到底在测什么、用什么环境测。
1.1 为什么单独把环境拎出来说
很多朋友拿到这类工具,习惯直接 npx 启动、跑几条命令就下结论,结果发现数据忽高忽低,根本没法写报告。其实大多数波动都来自环境变量不同:装了哪些浏览器、是不是首次下载、有没有禁用 GPU、MCP 服务是不是带缓存,每一项都会直接影响响应时间。我这次干脆把环境固定成一套“基线”,后面所有对比都在这套基线上做加减法,这样出来的数据才具备可比性。
基线环境我列一下:
- 开发机:某 Windows 11 笔记本,32GB 内存,8 核 i7,固态硬盘,外接显示器
- 运行时:Node.js 20 LTS,Python 3.11(只用于散点统计脚本)
- Playwright 版本:1.4x 系列,MCP 包用的是官方 @playwright/mcp
- 浏览器:Chromium、Firefox、WebKit 三套都由 Playwright 统一管理,另外单独留了一个系统 Chrome 用于对比
- 被测页面:本地搭建的一个模拟业务页面,包含首屏文本、按钮、输入框、弹窗等常见交互元素,避免公网网络波动干扰
这套环境不算特殊,也不属于“为了赢而优化”。日常做前端自动化、写爬虫、开发 LLM Agent 工具链的机器,差不多都是这个水平。
1.2 三种执行方式的配置细节
这次对比我锁定了三种方式:
第一种,官方默认启动。也就是直接跑npx @playwright/mcp@latest,MCP 服务起来之后,由工具内部自行管理浏览器生命周期。这是用户最容易上手的方式,但也是我怀疑耗时最不确定的方式,因为每次操作都要经过“MCP 服务调度 + Playwright 启动/复用浏览器上下文 + 页面交互”整条链路。
第二种,指定系统级 Chrome 启动。做法是给 MCP 传--browser chrome或者通过--executablePath指向已安装的 Chrome,实测时我会固定用这个配置对比默认 Chromium。
第三种,连接已有浏览器实例。通过 Playwright 的connectOverCDP连接到一个已经用远程调试端口打开的 Chrome。这种方式理论上省掉了“冷启动浏览器”的损耗,但前提是浏览器进程要一直活着,MCP 只做新标签页和交互操作。
三种方式对应的启动命令我放在后面第四节,先不铺开。
1.3 基线数据是怎么采集的
我给自己定了三条纪律:
- 每组测试循环 30 次,抛开前 5 次预热数据,避免“首轮要加载资源”导致的天然偏高
- 每次请求之间间隔 3 秒,避免 MCP 服务内部缓冲区或浏览器资源调度产生级联影响
- 所有请求都通过 MCP 客户端发出,不走 Playwright 原生脚本,因为我要测的是“MCP 通道下的响应时间”,而不是纯 Playwright 的底层速度
采集目标是三类时间:
- MCP 工具调用总耗时:从客户端发出请求,到收到最终响应之间的完整时间
- 解析阶段耗时:MCP 服务端解析工具调用参数、分配会话任务的时间
- 浏览器操作耗时:服务端实际驱动浏览器执行动作的时间,这一步靠服务端追加的日志时间戳来近似统计
统计脚本用 Python 写了个几十行的小工具,算中位数、P90、标准差,不只看平均值。原因后面有专题讲,这里先记住:均值在这种带长尾的场景里非常容易被极端值带偏。
2. 测试场景设计与用例编排
性能测试的结果好坏,一半取决于用例设计。我这次没有用 Playwright 自带的性能测试模版,而是从“日常 Agent 用得最多的操作”里挑了几条出来,尽量让测试贴近真实使用。
2.1 核心测试指令的挑选逻辑
MCP 给 LLM 暴露的能力虽然不少,但从实际日志看,90% 的调用集中在这些操作:打开页面、读标题、抓正文摘要、点击按钮、填表提交、截图。LLM 调用 MCP 工具链时,常常是“先打开 → 再读取 → 操作 → 截图反馈”,所以我挑了六条最典型的:
open_url——打开本地测试页get_title——获取页面标题extract_text——抽取页面主体文本click_element——点击页面内按钮fill_and_submit——向输入框填入内容并触发回填逻辑take_screenshot——截取首屏画面
每条指令单独测,不做组合串联,这样每个动作的耗时边界清晰,不会出现“上一步慢了拖累下一步”的干扰。
2.2 测试页面的结构设计
为了点击、填表这类动作稳定可复现,我用的不是任意网站,而是本地起了一个专门的测试页面。上面的元素固定:一个 h1 标题、一个有三个按钮的工具栏、一组输入框、一个会被点击后弹出文案的区域。所有交互都是纯前端实现,不涉及网络请求。这么做的原因很简单:性能测试不能引入不可控变量,线上页面随时可能改 DOM,公网延迟也会让“打开页面”这一项失真。
如果需要复现,你也可以用任意一个静态站点,只要保证元素稳定、不依赖登录态就好。实操上比较快的做法是写一个单 HTML 文件,用 Python 的http.server或者 Node 的serve在本地起服务,端口固定 8080,然后让 MCP 统一访问http://localhost:8080/bench.html。
2.3 每一轮测试的运行流程
我写了一段流水线脚本,大致是:
- 启动 MCP 服务,配置对应浏览器方案
- 通过 MCP 客户端建立一组会话
- 依次发送六条指令,每次记录返回的字节数和耗时
- 全部指令完成后关闭会话,等 3 秒再进行下一组
- 组间重启 MCP 服务,但不重启测试页服务
为什么要组间重启 MCP 服务?因为长时间运行的 MCP server 会积累大量上下文资源,尤其是截图和正文抽取会残留较大的内存数据,不及时清理的话后面几组的数据会受影响。这个问题在之前一次测试中真实发生过,后面踩坑章节会细讲。
3. 响应时间构成拆解与统计口径
数据全测出来之后,比对之前得先搞清楚一件事:MCP 的响应时间到底由哪几段组成?如果只是笼统地算一个“从发出请求到返回结果”,那最终拿到的数字很难定位到瓶颈。
3.1 MCP 链路的时间分段模型
一次 MCP 工具调用,内部走的链路大致是这样的:
- 客户端把工具名、参数编码成 JSON-RPC 消息,经 stdio 或 HTTP 通道传给 MCP server
- server 解析消息、校验参数,然后把对应工具调度到内部的执行队列
- 工具内部再调用 Playwright 的 API,通过浏览器驱动协议和真实浏览器通信
- 浏览器完成动作后,经过同样的路径返回结果
所以一次响应耗时 T 可以大致分解为:
T = T 传输 + T 解析调度 + T 工具逻辑 + T 浏览器操作 + T 序列化返回
T 传输在 stdio 模式下通常只有零点几毫秒,HTTP 模式下会高一些。T 解析调度一般在几毫秒到几十毫秒,主要取决于参数大小和 MCP server 是否有复杂的权限校验逻辑。大头往往集中在 T 工具逻辑 + T 浏览器操作,因为浏览器动作是真实世界里的重活——启动进程、加载页面、执行脚本、截图编码,哪一个都不便宜。
3.2 为什么平均值会骗人
我一开始就是用平均值做排序的,结果有一次因为某个极端值(浏览器崩溃重试导致 20 秒超时),平均值直接比中位数高了一大截,几乎把原本的对比结论颠倒过来。从统计角度说,响应时间在真实场景里是右偏分布,偶尔几次异常会让均值大幅抬高,中位数才更能代表“大多数情况下的体验”。所以后面的所有对比我都采用中位数为主、P90 为辅、标准差做稳定性判断。
P90 这个口径尤其重要。平均值告诉你“大概怎么样”,P90 告诉你“最差的时候怎么样”。实际给 LLM 用 MCP 时,超时阈值通常就看 P90,因为一次卡顿就可能导致 LLM 误判或重试,影响比预期更大。
3.3 判定“稳定”的标准
我在记录里还顺带算了标准差。标准差小,说明这套方案表现稳定,适合生产环境;标准差大,哪怕中位数好看,也得小心高峰期掉链子。
这次实验下来,三种执行方式对六个动作的数据波动差异非常明显。默认启动方式的标准差普遍高,尤其是第一次访问测试页时;CDP 连接方式的波动最小,因为浏览器实例早就热好,每次操作只是新开标签页的同进程动作。具体数据下一节给。
4. 三种浏览器执行方式的实测对比
铺垫做完了,直接上结果。所有数据均基于同一台机器、同一个测试页面、同一条指令序列,每项取 25 轮有效样本的中位数。
4.1 官方默认启动(Chromium 内核)的表现
官方默认启动对应命令是:
npx @playwright/mcp@latest --headless我用默认配置启动后,得到的中位数数据如下:
| 指令 | 中位数耗时(ms) | P90(ms) | 标准差(ms) |
|---|---|---|---|
| open_url | 1680 | 2210 | 340 |
| get_title | 320 | 430 | 60 |
| extract_text | 1380 | 1710 | 220 |
| click_element | 720 | 1120 | 180 |
| fill_and_submit | 950 | 1340 | 210 |
| take_screenshot | 1150 | 1620 | 280 |
open_url 接近 1.7 秒,这个数字并不意外。MCP 默认启动要创建浏览器上下文,还要执行页面导航并等页面主资源加载完。加上默认模式下有一些兜底逻辑,比如捕获页面控制台消息、监听请求事件,这些机制虽然会给 Agent 提供更丰富的信息,但代价就是每轮操作挂的钩子更多,耗时自然上去了。
get_title 只有 300 多毫秒,说明在浏览器上下文已经就绪的前提下,纯 DOM 操作是很快的。这也验证了时间主要耗在“上下文初始化”和“事件收集”而不是动作本身。
4.2 指定系统 Chrome 启动的收益
第二种方式是给 MCP 指定使用系统级 Chrome:
npx @playwright/mcp@latest --browser chrome --headless如果在 Windows 上无法自动发现 Chrome,可以补上:
npx @playwright/mcp@latest --browser chrome --executablePath "C:\Program Files\Google\Chrome\Application\chrome.exe" --headless结果如下:
| 指令 | 中位数耗时(ms) | P90(ms) | 标准差(ms) |
|---|---|---|---|
| open_url | 1390 | 1720 | 210 |
| get_title | 260 | 330 | 40 |
| extract_text | 990 | 1210 | 150 |
| click_element | 540 | 780 | 90 |
| fill_and_submit | 690 | 890 | 110 |
| take_screenshot | 930 | 1260 | 180 |
对比默认方式,整体耗时明显缩短。open_url 从 1680 降到 1390,extract_text 从 1380 降到 990,click_element 降幅接近 25%。
分析一下原因:系统 Chrome 的启动和首屏初始化比 Playwright 下载的 Chromium 构建要快一些,尤其在字体渲染、GPU 进程初始化等环节。另外,系统 Chrome 一般带缓存,本地测试页的 favicon、样式表会有命中,页面加载的附加网络请求也少。
不过这里有个隐藏细节:如果你机器上同时开了很多 Chrome 标签页,内存压力会导致数据反弹。我测试中关掉了无关标签页,实际部署时也需要提醒自己控制浏览器负载。
4.3 远程调试端口连接已有浏览器的表现
这个方案思路是:先把 Chrome 用调试端口启动,然后让 MCP 通过 CDP 连接同一个浏览器实例。
前提命令:
chrome.exe --remote-debugging-port=9222 --user-data-dir=C:\temp\mcp-profileMCP 侧连接方式:
npx @playwright/mcp@latest --cdp-endpoint http://localhost:9222实测数据:
| 指令 | 中位数耗时(ms) | P90(ms) | 标准差(ms) |
|---|---|---|---|
| open_url | 760 | 940 | 120 |
| get_title | 190 | 260 | 30 |
| extract_text | 620 | 820 | 100 |
| click_element | 390 | 520 | 70 |
| fill_and_submit | 480 | 620 | 80 |
| take_screenshot | 700 | 870 | 110 |
这个结果符合预期:open_url 直接砍到 760ms,和默认方式相比降低了 55% 左右。因为浏览器进程早就活着,MCP 连上之后只需要新开标签页、创建页面对象,不需要负担完整的浏览器启动成本。
但坑也在这里,CDP 方案的省时收益完全依赖“浏览器实例常驻”。一旦浏览器被手动关掉或崩溃,MCP 服务会报连接错误,重新拉起进程又要花时间。所以这个方案适合做长时间运行的 Agent 服务,不适合跑完就走的一次性任务。
4.4 三类方式的最终排序与结论
把三种方式的中位数套到一块看:
| 执行方式 | open_url | extract_text | click_element | 总评 |
|---|---|---|---|---|
| 默认启动 Chromium | 1680ms | 1380ms | 720ms | 适合一次性使用,配置最省事,性能最不稳定 |
| 指定系统 Chrome | 1390ms | 990ms | 540ms | 推荐日常使用,性价比高,性能和稳定都居中 |
| CDP 连接已有浏览器 | 760ms | 620ms | 390ms | 性能最好,适合常驻服务,但需要额外管理浏览器生命周期 |
我自己的结论:如果只是本地调试、临时跑几条指令,用第一种完全没问题;如果要做成一个供多个 Agent 并发调用的服务,建议直接上 CDP 连接方案,并配一个守护脚本定期检查浏览器进程状态。
5. 踩坑记录与排查心得
这一部分是我觉得整篇最有价值的内容。实验不是一帆风顺,中间出现了多次“数据异常”清洗重跑的案例。下面几条都是真实遇到过的,按频率从高到低列出来。
5.1 首轮数据虚高问题
第一次跑测试,我发现 open_url 的前三轮数据普遍在 2.3 秒以上,后面才回落到 1.6 秒左右。一度以为是 MCP server 冷启动导致的,后来单独验证,确认是 Playwright 内核首次运行时要创建各种缓存目录、解析浏览器二进制路径,加上操作系统文件缓存还没有生效。
处理方式简单粗暴:每组测试前先跑两轮“哨兵指令”,不计入统计。这是基准测试的常规操作,但新手容易忽略。以后写测试脚本时,我会在正式循环前加一段预热代码。
5.2 截图指令造成的慢速污染
截图操作本身耗时不高,但截图会产生较大的二进制数据,这个数据在 MCP 通道里要编码成 base64 返回。如果连续多次调用 screenshot,MCP server 和客户端之间的缓冲区压力会变大,导致后续指令排队延迟。
我没有在脚本里显式做清理,而是把每组测试之间的休眠时间从 1 秒加到 3 秒,并在每轮结束之后主动调用一次“读取页面标题”这种轻量级操作,当作缓冲区清空的探针。这个办法不一定普适,但至少能降低串扰。
5.3 MCP 并发调用暴露的上下文隔离问题
中间有一次我想顺手测并发能力,同时发出了 5 个提取正文的请求。结果发现总耗时比 5 个串行还慢,原因不是浏览器的渲染能力不足,而是 MCP server 默认在同一个浏览器上下文中串行执行工具。后面的请求要等前面的操作释放锁。
这个发现对实际使用很有帮助。如果你的 Agent 会同时调用多个 MCP 工具,最好在 client 侧限制并发数,或者直接给每个 Agent 会话分配独立的浏览器上下文,不要共享。
5.4 数据汇总时被平均值误导的教训
前文提过,我用平均值排名时出现过一次结论颠倒的尴尬经历。具体是默认启动方式中 open_url 有一轮耗时达到了 7 秒,直接把平均值拉高到了 2.1 秒,比系统 Chrome 方案的 1.5 秒高出很多。当时差点写下“默认方式性能远差于系统 Chrome”的结论。
后来改用中位数后才发现,默认方式的“典型值”其实没有那么大差距。所以这里给各位一个强烈建议:做 MCP 性能相关评测时,统一用中位数 + P90 + 标准差,不要光看平均数。如果汇报对象只认平均值,至少把 P90 也放上去,这样能体现那 5% 的极端情况对体验的实际影响。
5.5 浏览器进程残留导致的下轮数据异常
测试过程中还有一次,CDP 方式跑着跑着突然 turn 变得非常慢,单条 get_title 都要 1 秒多。查了半天,发现是之前手动打开的一个 Chrome 页面占用了 100% 的 CPU——某个视频站的后台跑了加密脚本。这在测试环境里属于典型的“脏数据源”。
解决办法是测试前统一检查 CPU 占用率、清掉无关标签页。如果机器资源本身紧张,建议用独立用户数据目录启动调试浏览器,像这种场景,一个干净浏览器进程的稳定度远比设备本身的绝对性能重要。
写在最后
这次基准测试做下来,我个人在实际操作中最深的一个体会是:MCP 的响应时间不只是“浏览器快不快”的问题,而是整条链路协同的问题。协议传输、服务调度、浏览器生命周期、统计口径,每一环都可能让最终数字翻倍。如果你跟我一样在搭 Agent 工具链,建议把 CDP 连接方式当成性能优先的默认方案,同时把超时阈值和告警建立在 P90 上,这样生产环境里的“偶发卡顿”才不会变成“日常故障”。
最后再分享一个小技巧:测试结论不要一次跑完就定案,最好连续跑三个时间段(上午、下午、晚上),因为硬件调度、后台任务、插电状态变化都会影响数据。隔天再复测一次同一组数据,能帮你区分哪些是真实性能差距,哪些只是环境噪声。后续如果有时间,我还打算把不同操作系统、不同网速模拟下的表现也补进来,给在复杂网络环境里用 MCP 的朋友多一份参考。