ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Playwright性能优化:10个技巧把47分钟测试压到12分钟

Playwright性能优化:10个技巧把47分钟测试压到12分钟 去年我接手过一套 Playwright 测试全量跑完要 47 分钟CI 排行榜里每跑一次要排队功能发布前大家都习惯先去冲杯咖啡再点“跑测试”按钮。后来花了差不多一周时间做性能优化把整套测试压到了 11 分 40 秒而且稳定性和误报率也同步降了很多当时只知道“加长超时”的同事后来都在反过来问我你到底动了什么其实就是今天要聊的这 10 个实用技巧。这篇文章不仅适合做 Web 自动化测试的同行也适合用 Playwright 写爬虫、做巡检脚本、维护 CI 流水线的人。标题里那三个关键词——Playwright、性能优化、测试时间说白了就是三件事减少无效等待、减少重复劳动、减少资源浪费。下面按我的排查顺序来写你先跟着做定位再去逐条调优。1. 先把时间账单算清楚慢在等待还是慢在脚本1.1 Trace Viewer 是最诚实的计时器做性能优化最忌讳凭感觉猜。我见过太多人一上来就改配置最后发现最耗时的不是脚本而是某个页面组件一直在转圈。Playwright 自带的 Trace Viewer 就是专门干这个的跑测试时把 trace 打开出报告后能看到每个 action 的起始时间、结束时间、等待了多久、请求了什么接口、控制台有什么报错。实际操作很简单命令行里加上--trace on也可以直接在 playwright.config.ts 里写trace: on跑一个小范围样例。测试结束后会生成一个trace.zip用npx playwright show-trace trace.zip打开就能看时间轴。时间轴上最刺眼的通常是“waiting for selector”和“waiting for navigation”这两类状态前者说明定位器有问题后者说明等待策略没选对。1.2 页面加载时间和测试代码开销要分开统计只看总时长不够因为报告里的 duration 是“等待时间 脚本执行时间”的混合体。最好的做法是先把单个用例用--repeat-each3跑几遍看波动范围。如果同一个用例每次耗时相差很大问题大概率在网络或环境如果每次都稳定在同一个数字那说明脚本里有固定等待。我习惯在优化前给每个 spec 文件打印一个简单标记比如在test.afterEach里用test.info().duration记录实际耗时再和 Trace Viewer 里的 action 时间对比。哪个 action 占了 80% 以上的时间就优先解决哪个。这一步做完下面 10 个技巧你至少能选出 3 个真正有效的来用。2. 浏览器与 Context 层级的提速原则2.1 技巧1并行执行workers 数量按 CPU 核数来配Playwright Test Runner 天生支持并行默认会按 CPU 核数的一半来跑。很多人没意识到这一点还在用一个 worker 串行跑几十条用例时间自然翻倍。建议在配置文件里显式设置// playwright.config.ts export default defineConfig({ fullyParallel: true, workers: process.env.CI ? 4 : undefined, });这里有个前提用例之间不能有全局共享的登录态或数据依赖。如果你早期把用例写成了“A 用例先建数据、B 用例再消费”那并行后必然互相干扰得先用后面说的storageState改造或者用test.describe.configure({ mode: serial })给有依赖的分组降级。不要为了并行而全开先保证每条用例独立再谈并行。2.2 技巧2storageState 复用登录态别让每个 worker 都重新登录登录是端到端测试里最贵的一步打开登录页、输入、等待跳转、可能还有 MFA。如果 8 个 worker 各跑 20 条用例等于登录 160 次。用storageState把登录后产生的 cookie 和 localStorage 存成文件后续用例直接复用。// 在 setup 或专用脚本里执行 const browser await chromium.launch(); const context await browser.newContext(); const page await context.newPage(); await page.goto(https://你的系统/login); await page.getByRole(textbox, { name: 用户名 }).fill(test); await page.getByRole(textbox, { name: 密码 }).fill(pass); await page.getByRole(button, { name: 登录 }).click(); await page.waitForURL(**/dashboard); await context.storageState({ path: ./auth.json }); await browser.close();然后在配置里给业务用例指定复用这个状态test.use({ storageState: ./auth.json });或者用 project 机制把登录流程做成一个独立 setup project业务测试项目自动依赖它这也是 Playwright 官方推荐的做法。复用登录态之后我这边整套用例平均每条省了 8~15 秒不等尤其是 worker 多的时候收益是指数级的。2.3 技巧3复用浏览器实例但每个测试都要独立 ContextPlaywright Test Runner 本来就是每个 worker 启动一个浏览器实例、每个测试新建一个 Context用完自动关。所以你不需要自己在beforeAll里 launch 一堆浏览器。最容易出问题的反而是手工脚本比如爬虫场景。如果是在 Node 脚本或 Scrapy 里集成 Playwright正确姿势是启动一个browser每次任务用browser.newContext()创建一个新独立环境做完任务context.close()。千万不要每次都chromium.launch()浏览器进程启动少说要几百毫秒加上新开页面、初始化 JS 引擎几十个任务叠加起来差距巨大。另外一个反直觉的点browser.newPage()其实是“newContext newPage”的快捷方式不细究的人会误以为它只是开个标签页。如果要在同源多个页面间共享 localStorage应该建一个 Context 再context.newPage()如果完全不共享那就分开 Context。想清楚这一层很多数据污染和耗时问题会一起消失。3. 等待策略十次超时里有八次是被固定等待拖垮的3.1 技巧4用 locator 的自动等待替换 sleep 和 waitForTimeout我审过的项目里出现最多的坏味道就是page.waitForTimeout(3000)甚至有人写 5 秒、8 秒。这种固定等待的问题在于页面 1 秒就渲染完了你白等 4 秒页面 10 秒才出来你又不够等。Playwright 的 action API 本身就带自动等待点击、填表、取文本前都会等到元素可见、稳定、可操作。// 错误示范先睡 5 秒再点 await page.waitForTimeout(5000); await page.getByRole(button, { name: 保存 }).click(); // 正确示范Playwright 会自己等到按钮可以点 await page.getByRole(button, { name: 保存 }).click();刚开始不习惯的人总觉得“不 sleep 就不安心”实际上 Playwright 内部的 actionability 检查比大多数 hand-made sleep 靠谱得多。把它删掉你会发现单条用例少个 2~4 秒是常态。3.2 技巧5用 expect 轮询替代“等 断言”两段式写法等待和断言经常被拆成两步先waitForTimeout再expect(...).toBeVisible()。其实expect(...).toBeVisible()自己有轮询能力会不断重试直到超时。把默认超时时间从 5 秒调到 10 秒很多偶发慢渲染的问题就不再需要额外等待。await expect(page.getByText(订单已创建)).toBeVisible({ timeout: 10_000 });这比手动循环else再“再等一秒”优雅得多。对应地如果你要断言列表数量直接用await expect(locator).toHaveCount(3)而不是先查count()再比较。toHaveCount是带轮询的断言天然能容忍渲染延迟而你自己写if (count 3)基本就是一次性快照容易误报。3.3 技巧6waitForResponse / waitForURL 比 networkidle 可靠得多很多人习惯page.waitForLoadState(networkidle)觉得“页面没网络请求了就说明加载完了”。但现代前端项目里这句十有八九是坑页面可能有定时轮询、埋点上报、长连接networkidle 要求 500ms 内没有网络连接于是它可能一直等不到直接把超时拉满。Playwright 官方文档里也明确不推荐依赖 networkidle。正确思路是等待业务信号接口返回、URL 变化、某个关键元素出现。await Promise.all([ page.waitForResponse(resp resp.url().includes(/api/order) resp.ok()), page.getByRole(button, { name: 提交订单 }).click(), ]);把“点击”和“等接口”放到Promise.all里是 Playwright 的标准写法能避免点击后页面跳转导致的事件监听错失。用这个思路替换掉 networkidle 之后我的测试里至少有三处从跑满 30 秒超时降到了 2~3 秒内完成。4. 定位、iframe、滚动与监听DOM 层面的隐形开销4.1 技巧7定位器要稳定且确定role/text 优先XPath 慎用定位器选不好不只是 flaky还会慢。原因很简单复杂 XPath 需要遍历 DOM 节点做条件匹配文本模糊匹配可能触发多个候选Playwright 还要解析候选列表并逐个判断可见性。性能上最稳的是有>// 推荐稳定且语义清晰 await page.getByRole(button, { name: /删除/ }).click(); // 不推荐从外层一路套下来的 XPath await page.locator(//div[classlist]/div[2]//button[contains(class,del-btn)]).click();如果你选择用业务文本定位尽量加{ name: /精确一点/ }或配合filter({ hasText: 唯一字符串 })避免“只要包含某个字就命中”的模糊匹配。还有一个小细节locator.count()会立刻执行一次 DOM 扫描在循环里用尤其伤断言数量时用expect(locator).toHaveCount(n)单次业务操作时直接操作第一个或根据文本过滤。4.2 技巧8iframe 用 frameLocator 锚定别反复遍历 frames页面里嵌入的 iframe比如第三方客服组件、Embedded Chat、广告往往是性能重灾区。最笨的写法是每次操作前page.frames()找一遍找到需要的 frame 再定位元素。如果 iframe 内容是动态渲染的这套逻辑会又慢又脆。正确做法是用frameLocator把目标 frame 固定下来让 Playwright 自动等待 iframe 和内部元素出现。const chatFrame page.frameLocator(#embedded-chat); await chatFrame.getByRole(textbox, { name: 消息 }).fill(hello);这里有个很微妙的点frameLocator不止能等内部元素它还能等 iframe 本身加载完成所以即使客服组件是延迟异步加载的也不会因为“找不到 frame”直接失败。用这个写法替代动态遍历page.frames()至少省掉每次操作前几百毫秒的查询开销。配合 Scrapy 抓动态 iframe 内容也是一样的逻辑同一个 context 里用 frameLocator 锁定目标别每抓一次就重新渲染整页。4.3 别忽略滚动、截图和事件监听器的附加消耗page.mouse.wheel慢慢滚到底的写法在截图用例里很常见其实大部分情况用locator.scrollIntoViewIfNeeded()就够了目标元素滚到视野中央即可不需要整页滚一遍。整页滚动会触发大量懒加载请求反而让页面比正常状态多加载几十张图。另外page.on(console)、page.on(request)这类事件监听器如果挂在全局且不做清理监听器会一直驻留每个请求进来都要跑一遍处理函数。尤其是处理函数里有正则匹配、字符串拼接、写日志用例多了以后 CPU 开销非常明显。规范做法是只在必要范围内挂载用完立刻page.off(console, handler)C# 等语言里也是同样的思路确定不听了就把解绑做掉。5. 资源、截图、日志给用例做一下“瘦身”5.1 技巧9用 route 拦截图片、字体、视频只给页面需要的流量端到端测试里常见页面商品图一坨一坨的每张图几百 KB虽然加载是并行的但在 CI 这种低带宽环境里资源大小会直接拖动作响应。Playwright 的context.route可以按需拦截请求对非必要大资源直接 abort。await context.route(**/*.{png,jpg,jpeg,webp,woff,woff2,mp4}, route route.abort() );这里注意两点一是如果你在测视觉回归或需要真实渲染完的页面就别拦图片和字体否则截图结果不对二是 abort 的是网络请求页面如果有 onerror 逻辑可能会弹错误提示得先跑一遍确认无副作用。我一般在购物类项目里只拦商品 SKU 图片和埋点请求实测整条流程能快 10%~20%。5.2 技巧10trace、video、screenshot 全程开着等于给每个用例加负重调试时开着这些开关很有用但生产 CI 里全程开 video 和 trace每跑一条用例都要额外录制、编码、落盘IO 和 CPU 都是实打实的开销。建议配置成按需记录export default defineConfig({ screenshot: only-on-failure, trace: on-first-retry, video: off, });on-first-retry的意思是用例第一次失败后重跑时开 trace这样你既能拿到失败现场定位问题平时跑通过的用例又不产生额外开销。如果你当前还在“所有用例都开视频”的阶段可以先看看 artifact 目录里有多少视频文件是根本没有打开过的我敢打赌超过九成。5.3 移动端仿真不是免费的不需要就别开在桌面测试里顺手给 context 加上isMobile: true、hasTouch: true、deviceScaleFactor: 2UI 会走移动端渲染分支触控事件模拟和双倍分辨率图片都会增加负载。除非你明确要测移动端样式否则别把这些参数当成默认配置。很多人不知道性能差距同样的用例桌面跑 5 秒开移动端仿真可能要 7~8 秒甚至更多。6. 开发阶段的时间也要算Codegen、MCP 和安装加速6.1 Codegen 生成的是草图不是最终用例npx playwright codegen https://目标地址能快速抓元素和操作但也容易生成一堆“看起来能用”的坏味道。我的经验是用 Codegen 探索页面、确认选择器生成后立刻做三件事——把定位器改成 role/getByTestId 的稳定形式、删掉自动生成的无意义 sleep、把“等固定时间”换成 waitForResponse 或 expect 轮询。Codegen 只解决“怎么操作”的问题性能优化永远要回到上面 10 条手工规则上来。6.2 用 MCP 把调试从“改代码-跑一遍-看报告”里解放出来最近圈子里的 Playwright MCP、Chrome DevTools MCP 我都试过这类工具最大的价值是让 AI 或本地调试工具直接驱动浏览器快速复现某个页面状态省去“改一行代码→跑完整条用例→再回来改”的循环。比如在 IDE 里连上 MCP可以直接让工具去点某个按钮、读取当前页面某个元素然后你再决定把哪段逻辑写进测试。这部分的收益主要体现在写测试的时间上而不是测试执行时间但开发节奏变快以后你会更愿意花时间优化定位器和等待策略而不是急着堆 sleep。6.3 浏览器下载和依赖安装的提速经验最后聊一个经常被忽略的环节npx playwright install chromium每次全量下载浏览器如果 CI 不缓存光安装可能就要吃掉几分钟。我的经验是把 Playwright 浏览器缓存目录常见是~/.cache/ms-playwright加入 CI 的缓存策略或者把官方浏览器包提前下载好后放到内网制品库CI 里走离线安装速度能快一个数量级。另外注意 Playwright 版本和浏览器版本是绑定的升级 Playwright 后通常需要重新下载对应版本浏览器。如果你不想下载 Chromium也可以让测试走系统已装的 Chrome配置里用channel: chrome同时设PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD1适合 CI 镜像里本来就有浏览器的场景。6.4 关于验证码等人工交互别把它们放进性能优化清单还有一个我踩过的坑测试环境里的滑块验证码、短信验证码这类人工交互不要在脚本里硬过。硬过滑块通常要模拟轨迹、做坐标计算本身又慢又脆就算过了遇到安全策略升级可能整个套件崩掉。正确做法是和开发约定一个测试环境专用开关或 Mock 接口把这些交互在测试环境里直接关掉。这跟性能优化其实是同一个思路能绕过的环节别硬扛能复用的状态别重算。做了上面这些调整后我又花了点时间把同样的问题排查了一遍先看 Trace Viewer再砍固定等待接着并行、复用登录态、瘦身资源。最后结果就是从 47 分钟降到了不到 12 分钟。优化 Playwright 测试时间这件事没有什么高深魔法无非是把每一秒花在真正需要的地方。建议你本周挑一条跑得最慢的用例照着这个顺序走一遍大概率也能榨出不少水分。
返回列表