大多数性能问题都落在几个类别里:测试类型选错、反复登录的开销、缓慢的真实网络调用、臃肿的 CI 准备流程,以及资源受限的机器。本指南先诊断这些问题,再讨论解决办法。
从零开始时,先向 Cypress Cloud 记录一次基准运行,再按下面的优先级清单逐项处理。已有具体症状时,先用诊断工具确定瓶颈,再跳到对应策略。
测试速度为什么重要
慢测试会改变团队的工作方式。一次 CI 运行达到 30 分钟或更长时,开发者会停止等待反馈,把无关改动合在一起,失去测试原本带来的紧密迭代循环。快速测试则支持频繁、低摩擦的反馈,让回归问题在引入后不久就被发现。
基准
可以用以下范围评估测试套件。如果持续超出健康范围,就查看对应层次的加速策略。这些是原文给出的参考范围,并非所有应用都能达到的保证。
单个测试时长
| 时长 | 判断 |
|---|---|
| < 3 秒 | 很好,常见于桩响应与程序化准备状态的测试。 |
| 3—10 秒 | 可接受,常见于访问真实服务器的端到端测试。 |
| 10—30 秒 | 需要调查,可能有多余等待或繁重的 UI 准备流程。 |
| > 30 秒 | 较差,原文认为很可能有架构问题。 |
组件测试应稳定在每个 2 秒以内。测试位于 10—30 秒范围时,先检查测试层次选择,并用 cy.session() 缓存身份验证。
spec 文件时长
一个 spec 文件包含一个或多个测试。并行化时,Cypress 将整个 spec 分配给不同 CI 机器。因此,一个很长的 spec 可能让其他机器在大部分套件完成后空等。
| 时长 | 判断 |
|---|---|
| < 1 分钟 | 很好,内存压力低,适合并行。 |
| 1—3 分钟 | 可接受,常见于围绕单个功能组织的 spec。 |
| 3—5 分钟 | 需要调查,可能成为并行瓶颈,考虑拆分。 |
| > 5 分钟 | 较差,应拆成更小的 spec 或减少共享准备开销。 |
尽量让 spec 时长相近,使并行机器差不多同时完成。不到 10 秒的 spec 很少能从继续拆分中获益,因为浏览器启动、视频编码等固定开销往往超过节省的时间。关于边界,以及页面、区域或表单何时值得单独建文件,见一个 spec 应放多少内容。
完整套件时长
| 套件规模 | 串行目标 | 4 台及以上机器并行目标 |
|---|---|---|
| < 50 个测试 | < 3 分钟 | — |
| 50—200 个 | < 10 分钟 | < 3 分钟 |
| 200—500 个 | 15—30 分钟 | < 10 分钟 |
| 500 个以上 | 使用并行化 | < 15 分钟 |
诊断测试套件
优化之前,先确定时间花在哪里。
最慢的测试
在 Cypress Cloud 打开 Slowest Tests 分析报告。它按时长排序,最长的测试最值得简化或重构。
最慢的 spec
打开一次运行,进入 Specs,切到 Bar Chart。最长的柱条对应最值得拆分或简化的 spec。点击 spec 可查看各个测试结果。
不稳定的测试
每次运行都重试的测试,每次都消耗时间。在 Cloud 的 Flaky Test Management 中检查最高不稳定率的测试。频繁重试是一项要解决的技术债,不应永久接受。
过度测试的 UI
打开运行的 UI Coverage,在 Tested elements 中按交互次数排序。交互次数远高于元素重要性的情况,表明许多测试只是经过这个元素,并非刻意测试它。下文介绍解决方法。原文同时提供申请试用、演示和示例项目入口。
并行机器是否均衡
增加机器却未获得预期提速时,打开 Cloud 运行的 Machines 视图。它按机器显示所执行的 spec 及耗时。一台已空闲、另一台还在运行长 spec,是并行效果不佳最常见的原因。
CI 中的 CPU 与内存
启用 DEBUG=cypress:server:util:process_profiler 后运行 Cypress App,可以每 10 秒打印 CPU 和内存消耗。原文将 CPU 持续高于 100% 视为机器饱和的信号。大多数 CI 服务也提供资源利用率图。原文的命令组件设置为 env="DEBUG=cypress:server:util:process_profiler"、command="run"。
可用系统资源
运行 Cypress 的 info 命令,还可查看:
node -p 'os.cpus()'
优先级清单
按下面顺序优化,通常收益最大的项目在前:
- 升级到最新 Cypress。
- 选择正确测试层次;将 E2E 移到组件或 API 层,往往收益最大。
- 用
cy.session()缓存身份验证,原文估计每个测试节省 2—5 秒。 - 用网络桩,把 100—2000ms 的真实调用替换为低于 20ms 的响应。
- 程序化准备状态,把数分钟 UI 导航替换为数秒 API 调用。
- 并行化;原文将其描述为随机器数量线性扩展。
- spec 优先级与自动取消,尽早暴露失败,降低失败构建成本。
- 关闭视频并使用 Test Replay,减少每个 spec 的编码开销。
- 用
@cypress/grep标签分层执行 CI;每次 PR 运行快速冒烟或关键测试,grepFilterSpecs不加载没有匹配测试的 spec。 - 缓存 Cypress 二进制,避免每次下载 100MB 以上的数据。
- 给 CI 足够资源;资源不足往往表现为缓慢或不稳定,应先解决再调整其他 CI 配置。
- 去除任意时长的
cy.wait()。 - 精简 support 文件和导入,避免每次 spec 启动都打包无用代码。
- 用
blockHosts屏蔽分析、监控等第三方请求,屏蔽后立即返回。 - 避免通配符
cy.intercept('*'),只拦截测试需要的请求。 - 使用明确 DOM 选择器,避免
*、div先收集全部节点再过滤。 - 使用现代可见性算法;Cypress 16 默认采用较快的
checkVisibility()方案,复杂 DOM 尤其受益。 - 谨慎重试,
runMode: 1可缓冲不稳定失败,次数应低,并用不稳定数据修复根因。 - 用 UI Coverage 找到被重复测试的元素。
- 用
cy.prompt降低选择器维护开销;缓存命令在 CI 中运行时没有 AI 调用开销。
加速测试的策略
保持 Cypress 更新
调整配置或重构之前,确认使用最新版本。团队持续发布性能改进,旧版本无法获得这些收益。例如升级至 16.0.0,可以获得测试之间自动回收浏览器内存(参见 manageBrowserMemory)、更快的可见性检查、默认输入按键提速及其他改进。
npm:
npm install cypress@latest --save-dev
yarn:
yarn add cypress@latest --dev
pnpm:
pnpm add --save-dev cypress@latest
评估其他优化之前,先查看变更日志,所需修复可能已经在新版本中。
选择正确测试层次
影响最大的性能决策,是为每个场景选择合适的测试类型。组件或 API 测试能够完成的工作,如果写成完整 E2E,就会在套件里积累巨大开销。测试金字塔很有用:底层大量快速隔离测试,中间较少的集成测试,顶层只保留关键用户旅程的完整 E2E。
▲
/E2E\ ← Slowest: full stack, real server, real routing
/─────\ Use for critical user paths only
/ Comp \ ← Medium: UI components in isolation, no server
/─────────\ Use for component behavior, visual states, events
/ API Tests \ ← Fast: HTTP calls, no browser, no UI
/─────────────\ Use for backend contracts, data validation
/───────────────\
用 cy.request() 做 API 测试
cy.request() 直接从 Cypress Node 进程发送 HTTP 调用,不渲染浏览器、不操作 UI,也无需完整应用栈。原文指出,本地 API 调用可在 100ms 内返回。
// Testing that the API correctly rejects duplicate emails:
// no need to drive a registration form through the UI
it('rejects duplicate email registration', () => {
cy.request('POST', '/api/users', {
email: 'test@example.com',
password: 'pw',
})
cy.request({
method: 'POST',
url: '/api/users',
body: { email: 'test@example.com', password: 'other' },
failOnStatusCode: false,
})
.its('status')
.should('eq', 422)
})
API 测试适合验证请求响应契约、校验规则、为其他测试准备数据,以及通过 API 而非 UI 表现的行为。
Cypress Component Testing
组件测试直接在浏览器里挂载 UI 组件,无需完整应用服务器、路由或全局状态。原文估计它通常比相应 E2E 快 5—10 倍,每个约 1—2 秒。
// Testing a button component in isolation: no server, no routing, no auth
import { mount } from 'cypress/react'
import { SubmitButton } from './SubmitButton'
it('shows a loading spinner while submitting', () => {
mount(<SubmitButton isLoading={true} label="Save" />)
cy.get('[data-cy="spinner"]').should('be.visible')
cy.get('[data-cy="label"]').should('not.exist')
})
它适合单个组件行为、由 prop 决定的视觉状态、用户交互事件,以及无需后端运行的 UI 逻辑。如果某个 E2E 已完全替换网络请求,只测试一个组件的渲染,组件测试几乎总是更合适:更快、隔离更好,也更易维护。
端到端测试
E2E 使用完整栈:真实浏览器、服务器、路由与网络。它是最慢的一类,原文给出每个通常 3—15 秒,维护成本也最高。应保留给完整集成验证的价值超过成本的场景。
End-to-end tests are the right choice when:
✅ The critical user journey spans multiple systems (auth, API, database, email)
✅ You're testing a cross-system flow that can't be meaningfully stubbed
✅ You need confidence that the full stack works together before deploying
Component or API tests are a better fit when:
✅ You're testing UI behavior in isolation from the server
✅ You're testing an API contract without caring about the UI
✅ The scenario can be reproduced fully with mocked or stubbed data
健康的分布
| 类型 | 占比 | 典型时长 |
|---|---|---|
API(cy.request()) |
30—40% | 每个 < 100ms |
| 组件 | 30—40% | 每个 1—2 秒 |
| E2E | 20—30% | 每个 3—15 秒 |
确切比例取决于应用,但原则相同:移到组件或 API 层的测试会更快、运行成本更低,也更容易保持通过。
全局设置 baseUrl
不设置时,Cypress 启动先加载空白页,首次 cy.visit() 再重新加载。每个文件开头会浪费数百毫秒:
{
e2e: {
baseUrl: 'http://localhost:1234', // ✅ Faster approach: set a global baseUrl
},
}
设置后可直接打开应用,详情见全局 baseUrl。更重要的是,Cypress 会在运行测试前检查它是否可达。不可达时立即退出并明确报错,让服务器故障或错误环境变量在开始时暴露,而不是十分钟后一串令人困惑的失败。
缓存身份验证
填写表单、提交并等待跳转的完整登录通常每次 2—5 秒。100 个测试重复执行,会增加 3—8 分钟纯登录开销:
// ❌ Slower approach: full UI login in beforeEach
beforeEach(() => {
cy.visit('/login')
cy.get('[data-cy="username"]').type('admin@example.com')
cy.get('[data-cy="password"]').type('password123')
cy.get('[data-cy="submit"]').click()
cy.url().should('include', '/dashboard')
})
cy.session() 在登录后保存 cookies、localStorage 和 sessionStorage 等完整浏览器上下文,后续测试恢复快照,不重复登录。首次需 3 秒建立的会话,之后可在毫秒级恢复:
// ✅ Faster approach: cache authentication with cy.session()
Cypress.Commands.add('login', (username, password) => {
cy.session(
[username, password],
() => {
cy.visit('/login')
cy.get('[data-cy="username"]').type(username)
cy.get('[data-cy="password"]').type(password)
cy.get('[data-cy="submit"]').click()
cy.url().should('include', '/dashboard')
},
{
validate() {
cy.getCookie('session').should('exist')
},
}
)
})
beforeEach(() => {
cy.login('admin@example.com', 'password123')
cy.visit('/dashboard')
})
原文将其视为大多数团队最有收益的单项改变。200 个测试、每次登录 3 秒的串行套件,可少花 10 分钟。
用程序准备状态,减少 UI 准备
跨多个页面建立状态既慢又脆弱。用 cy.request() 或 cy.task() 直接准备,再跳到被测页面。
API 数据准备
// ❌ Slower approach: navigating through UI to create test data
beforeEach(() => {
cy.login('admin@example.com', 'password123')
cy.visit('/products/new')
cy.get('[data-cy="name"]').type('Widget Pro')
cy.get('[data-cy="price"]').type('49.99')
cy.get('[data-cy="save"]').click()
cy.url().should('match', /\/products\/\d+/)
})
// ✅ Faster approach: seed data via API, then visit the page directly
beforeEach(() => {
cy.login('admin@example.com', 'password123')
cy.request('POST', '/api/products', {
name: 'Widget Pro',
price: '49.99',
}).then(({ body }) => {
cy.visit(`/products/${body.id}`)
})
})
数据库数据准备
直接写数据库、文件操作或第三方服务准备等无法经过 HTTP API 的操作,使用 cy.task(),在配置中运行 Node.js 代码:
import { defineConfig } from 'cypress'
import { db } from './src/db'
export default defineConfig({
e2e: {
setupNodeEvents(on) {
on('task', {
// ✅ Faster approach: seed data via API, then visit the page directly
async seedUser(userData) {
const user = await db.users.create(userData)
return user.id
},
async resetDatabase() {
await db.truncateAll()
return null
},
})
},
},
})
beforeEach(() => {
cy.task('resetDatabase')
cy.task('seedUser', { email: 'test@example.com', role: 'admin' })
})
原文指出,直接数据库操作通常比通过 UI 完成同样动作快 10—100 倍。
清理优先放在 beforeEach
afterEach 在失败时也运行,访问服务器会让每次测试切换增加延迟。更重要的是,中途重新加载 Cypress 时,它根本不会运行,状态会不一致。移到 beforeEach,无论上一次通过、失败或中断,每次测试开始前都能清理:
// ❌ Anti-pattern: cleanup in afterEach
afterEach(() => {
cy.request('POST', '/api/reset-db')
})
// ✅ Faster approach: cleanup in beforeEach
beforeEach(() => {
cy.request('POST', '/api/reset-db')
})
详情见 after/afterEach 钩子。
将相关断言放在一起
每个测试只放一个断言,会让 Cypress 为每个断言都重置浏览器上下文、运行钩子并导航:
// ❌ Slower approach: unnecessary overhead from per-test browser resets
describe('user profile', () => {
beforeEach(() => {
cy.visit('/profile')
})
it('shows the avatar', () => {
cy.get('[data-cy="avatar"]').should('be.visible')
})
it('shows the username', () => {
cy.get('[data-cy="username"]').should('be.visible')
})
it('shows the email', () => {
cy.get('[data-cy="email"]').should('be.visible')
})
})
// ✅ Faster approach: group related assertions in a single test
it('shows profile information', () => {
cy.visit('/profile')
cy.get('[data-cy="avatar"]').should('be.visible')
cy.get('[data-cy="username"]').should('be.visible')
cy.get('[data-cy="email"]').should('be.visible')
})
每次测试都有这些固定成本。合并相关断言可以去除重复开销,同时保持清晰。更多讨论见仅有一个断言的小测试。
有策略地使用网络桩
等待真实 API 响应会受数据库、下游服务和网络延迟限制。不专门测试服务器行为时,可替换响应:
// ❌ Slower approach: test waits for a real API call that takes 800ms+
cy.visit('/dashboard')
cy.get('[data-cy="transactions"]').should('have.length.greaterThan', 0)
// ✅ Faster approach: stub the response: returns in < 20ms
cy.intercept('GET', '/api/transactions', { fixture: 'transactions.json' }).as(
'getTransactions'
)
cy.visit('/dashboard')
cy.wait('@getTransactions')
cy.get('[data-cy="transactions"]').should('have.length.greaterThan', 0)
真实 E2E 保留给登录、结账及核心流程,其余关注 UI 的测试使用桩:
// ✅ Faster approach: only a handful of tests need real server responses
it('creates a new order (integration)', () => {
cy.request('POST', '/api/orders', { productId: 1, qty: 2 }).then(
({ body }) => {
expect(body).to.have.property('orderId')
}
)
})
// ✅ Faster approach: the majority of UI tests can use stubs
it('shows an error message when the order fails', () => {
cy.intercept('POST', '/api/orders', {
statusCode: 422,
body: { error: 'Insufficient stock' },
}).as('createOrder')
cy.visit('/checkout')
cy.get('[data-cy="place-order"]').click()
cy.wait('@createOrder')
cy.get('[data-cy="error-message"]').should('contain', 'Insufficient stock')
})
原文的参考值是:桩低于 20ms,本地开发服务器真实请求通常 100—500ms,远程预发布服务器每次可能 500ms—2 秒。何时使用桩、何时真实请求,见网络请求指南。
只拦截需要的请求
通配符观察或拦截全部 HTTP 请求会有实际开销,每个匹配请求都暂停并交给测试代码后才继续。一次页面加载匹配几十或几百请求,这个成本会在整个套件累积:
// ❌ Anti-pattern: intercepts every request; Cypress processes each one
cy.intercept('*').as('anyRequest')
// ✅ Faster approach: only intercept the specific requests your test needs
cy.intercept('GET', '/api/users*').as('getUsers')
cy.intercept('POST', '/api/orders').as('createOrder')
页面发送 50 个图片、分析、特性标志轮询和错误监控请求时,* 会让全部 50 个都经过处理器。只拦截测试明确需要的请求。
用 blockHosts 屏蔽第三方请求
生产应用常加载分析、Sentry/Datadog 错误监控、A/B 测试和聊天插件。测试环境中,这些与被测行为无关的请求仍消耗时间。用 blockHosts 在网络层屏蔽域名,请求不会发出,Cypress 立即以 503 响应:
// ✅ Faster approach: block third-party requests with blockHosts
{
e2e: {
blockHosts: [
'*google-analytics.com',
'*sentry.io',
'*datadoghq.com',
'*.optimizely.com',
'*.intercom.io',
],
}
}
它接受字符串或字符串数组,以 minimatch 匹配通配符。每个被屏蔽请求的 x-cypress-matched-blocked-host 响应头标明匹配规则,便于查找误拦截。
使用明确选择器
过宽的选择器迫使 Cypress 先收集评估整个匹配集合,再过滤:
// ❌ Anti-pattern: queries every element in the DOM, then filters
cy.get('*').filter('[data-cy="submit"]')
// ✅ Faster approach: query specifically for the element you need
cy.get('[data-cy="submit"]')
*、div、section 匹配成百上千节点,会给浏览器查询与 Cypress 元素处理增加无用工作。应使用能唯一定位目标的最具体选择器。
精简 support 与 spec 导入
测试前,Cypress 预处理、编译并打包 support/spec 文件,所有导入都会进入包。cypress/support/e2e.js 每个 spec 前都运行,所以是风险最高的位置,新增导入每次启动都付出成本。
不把仅供 Node.js 使用的模块放进浏览器包
数据库驱动、fs、path、ORM 和服务端 SDK 会迫使打包器报错,或添加繁重 polyfill:
// ❌ Anti-pattern: importing Node.js-only code into a support file
import { db } from '../../src/db' // database driver: Node.js only
import fs from 'fs' // built-in: not available in browser
import { sendEmail } from '../../src/mailer' // server SDK: Node.js only
// ✅ Faster approach: move Node.js code to cy.task() in cypress.config.js:
// task code runs in Node and is never bundled into the browser
setupNodeEvents(on) {
on('task', {
async seedDatabase() {
await db.seed()
return null
}
})
}
完整 API 见 cy.task() 文档。
避免把整个代码库带进来的 barrel 导入
从集中再导出的 barrel 文件导入单个具名导出,也会让打包器处理该文件再导出的所有模块,包括根本没用的模块:
// ❌ Anti-pattern: barrel import drags in the entire utils module graph
import { formatDate } from '../../src/utils'
// ✅ Faster approach: import directly from the specific file
import { formatDate } from '../../src/utils/formatDate'
精简的 support 文件
// Custom commands and queries
import './commands'
// Third-party Cypress plugins
import '@cypress/code-coverage/support'
// Global hooks that apply to every test
beforeEach(() => {
cy.task('resetDatabase')
})
其他内容应放在 cypress.config.js 中作为 task,或由需要它的单个 spec 直接导入。
避免任意等待,调整超时
最常见的性能反模式是固定等待若干毫秒:
// ❌ Slower approach: wastes time even when the element appears in 200ms
cy.get('[data-cy="results"]').click()
cy.wait(3000)
cy.get('[data-cy="modal"]').should('be.visible')
// ✅ Faster approach: let Cypress retry until the assertion passes
cy.get('[data-cy="results"]').click()
cy.get('[data-cy="modal"]').should('be.visible')
想使用 cy.wait(number) 时,正确办法几乎总是增加 Cypress 可重试的明确断言,更多例子见多余等待。默认命令超时是 4 秒(defaultCommandTimeout)。应当几乎立即完成的操作,降低超时可更快失败:
// ✅ Faster approach: a synchronous DOM assertion after a user action should not take 4 seconds
cy.get('[data-cy="toast-success"]', { timeout: 1000 }).should('be.visible')
大型文件上传或慢报告等已知有延迟的操作,应提高超时,而不是固定等待:
// ✅ Faster approach: give slow report generation up to 30 seconds
cy.get('[data-cy="report-ready"]', { timeout: 30000 }).should('be.visible')
使用现代可见性算法
每次检查或断言元素可见,都要运行可见性检测。深层复杂 DOM 中,旧算法沿祖先树逐层读取 CSS,反复触发布局重算,成本可能很高。
Cypress 16 默认使用现代算法,交给浏览器原生 Element.checkVisibility(),速度更快,也让可见性的含义与浏览器一致。若 Cypress 15 曾启用 experimentalFastVisibility,现在应移除,因为现代算法已默认启用。
升级后若测试依赖旧算法特有的语义,例如祖先 overflow 裁剪、transform 隐藏或 fixed/sticky 遮挡检查,应优先改写断言,验证相同的用户可见行为。临时迁移可用 visibilityStrategy 切回旧算法:
{
visibilityStrategy: 'legacy' // temporary migration path only
}
也能在套件或单个测试上设置,逐步迁移:
describe('Dashboard', { visibilityStrategy: 'legacy' }, () => {
it('legacy edge case', () => {
// uses legacy algorithm for this suite
})
})
大量可见性断言、或很大且深层的组件树,收益尤其明显。
谨慎使用测试重试
重试能让不稳定测试不阻断 CI,但配置不当,执行成本会迅速累积。
对团队推进速度的帮助
没有重试时,不稳定测试失败会让整次 CI 失败。开发者必须注意到、调查、判断是否真实回归,再手动重新运行。原文估计每次可能花 30—60 分钟,繁忙团队还会积压被阻塞的 PR。启用后,Cypress 自动按配置追加尝试。第一次失败、第二次通过会标为通过,继续运行;Cloud 用 Flaky 标签显示这些自动恢复测试,方便追踪最频繁的重试。
执行成本
每次重试完整执行测试,包括 beforeEach 与 afterEach。retries: 2 最多执行三次才判失败。
| 测试时长 | 重试 | 每个不稳定测试最坏额外耗时 |
|---|---|---|
| 5 秒 | retries: 1 |
+5 秒 |
| 5 秒 | retries: 2 |
+10 秒 |
| 15 秒 | retries: 2 |
+30 秒 |
| 15 秒 | retries: 5 |
+75 秒 |
若套件有 10% 测试不稳定,全局高重试次数能给每次 CI 增加许多分钟。
有目的地配置
CI 与本地开发分开设置。cypress open 应立即暴露失败以便修复;cypress run 少量重试可提供缓冲:
{
retries: {
runMode: 1, // ✅ Faster approach: one retry in CI catches most transient flake
openMode: 0, // no retries locally: fail fast so you fix the root cause
},
}
原文认为 runMode 一次重试可应对绝大部分真正的短暂不稳定。特定区域较不稳定,也可在套件层设置:
describe('Payment flow', { retries: { runMode: 2, openMode: 0 } }, () => {
it('processes a card payment', () => {
// ...
})
})
重试提供兜底,却不会修复根因。用 Cloud 的不稳定检测确定优先级,最高不稳定率和最多重试的测试带来最大累积开销。
用 UI Coverage 消除过度测试
套件自然增长时,欢迎页、引导弹窗或 cookie 提示会因为每个 spec 都访问页面而被反复操作,尽管测试并非刻意验证它。只需验证一次的 UI,可能累积几十个测试交互。UI Coverage 除了告诉你哪里没有测试,也用过高交互次数找出重复覆盖。原文在这里包含 UI Coverage 高级功能说明组件;具体套餐条件应查看官方产品说明。
寻找过度测试的元素
在 Cloud 已记录运行的 UI Coverage → Tested elements 中按交互次数排序,还能看到出现的快照数。50 个测试中交互 5 次可能合理;首次欢迎页的 Continue 按钮交互 184 次,说明大部分测试只是经过它。
验证一次,其余绕过
先写一个刻意覆盖完整流程的测试,确认欢迎页出现、内容正确且继续操作有效。其他测试通过自定义命令设置 cookie、localStorage 标志或调用 API,标记用户已看过这个流程:
// One test verifies the welcome flow end-to-end
it('shows the welcome screen to first-time visitors', () => {
cy.visit('/')
cy.contains('Welcome').should('be.visible')
cy.get('[data-cy="continue"]').click()
cy.contains('Dashboard').should('be.visible')
})
// All other tests skip it programmatically
Cypress.Commands.add('skipWelcome', () => {
cy.setCookie('welcome_dismissed', 'true')
})
// In every spec that doesn't test the welcome screen
beforeEach(() => {
cy.visit('/')
cy.skipWelcome()
})
这种 cookie/标志方案模拟回访用户的实际状态。修改测试并记录新运行后,报告应显示快照数和交互次数下降,接近真正需要测试的次数,被绕过流程的测试也会更快开始结束。
用 cy.prompt 减少选择器维护
类名、data 属性或组件结构变化,会让引用旧选择器的测试失败。诊断更新需要时间,常常发生在所有人等待的 CI 运行中。cy.prompt既减少最初编写选择器的时间,也降低 UI 变化后的修复成本。
首次运行后的缓存
首次执行会调用 AI 模型,解释自然语言步骤、评估 DOM 并生成 Cypress 命令。代码缓存后在机器和 CI 环境间共享,后续运行直接使用缓存,不调用 AI:
// First run: AI generates the commands and caches them
// All future runs: cache is used, no AI call
cy.prompt([
'Visit /products',
'Filter by category "Electronics"',
'Sort by price high to low',
'Verify the product count is 25',
])
自动修复选择器
缓存选择器失效时,cy.prompt 在下一次运行自动修复。Command Log 与 Cloud 结果会显示标签,说明哪些步骤修复、最终解析到哪个元素。它需要 Cypress Cloud 账户,原文说明所有套餐可使用,限制和配置见完整参考文档。
CI 中的提速
单机串行有上限。套件超过 10—15 分钟时,应考虑将工作分配到多台机器。
并行化
并行化将 spec 分配到多台 CI 机器,Cloud 根据历史时长编排。原文指出增加第二台通常使总时间约减半,继续增加机器能进一步获益:
cypress run --record --key=<record-key> --parallel
Kitchen Sink 示例从串行 1:51 变成两台机器 59 秒,降低 53%。大型套件通常用 4—8 台可降到 10 分钟内;当浏览器启动、视频编码等每个 spec 的开销超过实际测试时间时,收益递减。
用 Machines 视图检查分布。一台 3 分钟结束,另一台 12 分钟仍在运行,说明不均衡,先解决分布问题,增加机器才有效。
给 CI 足够资源
Cypress、浏览器、应用服务器及后台依赖同时运行,争抢 CPU 和内存会减慢测试。资源不足难诊断,因为它表现为不稳定、缓慢或随机失败,而非明确配置错误。
资源不足的迹象
- 运行越久,测试越慢。
- 浏览器中途崩溃,没有明确测试失败。
- CI 分析显示 CPU 或内存持续接近 100%。
- 视频卡顿或丢帧(原文建议改用 Test Replay)。
可以用前面的 profiler 调试日志确认。
资源达到上限后怎么办
提升运行 Cypress 的作业资源级别,具体方法查看 CI 提供商文档。原文给出的 Real World App 参考:CircleCI resource_class: large,4 vCPU、8GB RAM;GitHub Actions 标准 Ubuntu runner,4 vCPU、16GB RAM。还可查看 manageBrowserMemory,了解 Cypress 如何管理浏览器内存及何时适合关闭该功能。
spec 优先级
Spec Prioritization优先执行上次失败的 spec,配合自动取消,能更早暴露失败,减少已失败运行的机器分钟数。通常第 20 分钟才发现的问题,失败 spec 先运行时可能 2—3 分钟就发现。
自动取消
Auto Cancellation达到配置的失败数量后终止运行。构建损坏、环境变量缺失、服务未启动或部署错误等系统性失败,继续跑 200 个测试价值不大,应尽早暴露根因。
关闭视频,使用 Test Replay
设置 video: true 且记录到 Cloud 时,每个 spec 都会捕获视频,在下一个 spec 开始前压缩上传。CI CPU 有限,压缩成本尤其高;即使全部通过、无人观看,也要付出成本。Cypress 默认关闭视频,因此此建议只适用于当前已启用的套件:
{
video: false // ✅ Faster approach: disable video, use Test Replay instead
}
用 Test Replay 调试
启用视频通常是为了失败时有东西可看。Test Replay提供交互式时间回溯调试,可以在任何时刻查看 DOM、网络请求响应、控制台日志和 JavaScript 错误,还原 CI 中发生的情况。原文说明所有 Cloud 套餐均可使用,无额外费用,无需配置改动,记录到 Cloud 的运行自动可用:
Video: passive playback of the screen
Test Replay: full interactive access to DOM, network, console, and errors at every moment of the run
产品演示视频可查看。Replay 捕获结构化事件数据而非编码视频帧,通常比压缩视频小得多。标准输出可查看上传数据量与耗时,原文例子:
(Uploading Cloud Artifacts)
- Video - Nothing to upload
- Screenshot - Nothing to upload
- Test Replay - 298 kB
Uploading Cloud Artifacts: .
(Uploaded Cloud Artifacts)
- Test Replay - Done Uploading 298 kB in 633.40ms 1/1
在合适时机运行合适测试
不是每个 CI 事件都需要完整套件。功能分支推送需要快速反馈,合并到 main 值得完整测试。每个事件都运行全部测试可能浪费时间并拖慢反馈。官方 @cypress/grep 按标题或标签过滤,配合标签策略可以让 PR 跑快速冒烟测试,合并前跑综合套件。
安装配置
将 @cypress/grep 安装为开发依赖,原文以包管理器组件提供该操作。在 support 文件注册:
import { register as registerCypressGrep } from '@cypress/grep'
registerCypressGrep()
配置中添加 Node 插件以启用 spec 预过滤,这是下文关键性能特性所必需:
import { plugin as cypressGrepPlugin } from '@cypress/grep/plugin'
export default defineConfig({
e2e: {
setupNodeEvents(on, config) {
cypressGrepPlugin(config)
return config
},
},
})
给测试加标签
通过测试配置对象给测试及 describe 块添加标签:
it('logs in with valid credentials', { tags: '@smoke' }, () => { ... })
it('exports a report as CSV', { tags: ['@regression', '@slow'] }, () => { ... })
describe('Checkout flow', { tags: '@critical' }, () => {
it('processes a card payment', () => { ... })
it('applies a discount code', () => { ... })
})
| 标签 | 运行时机 |
|---|---|
@smoke |
每个 PR,快速覆盖关键路径。 |
@critical |
每个 PR,必须始终通过的核心流程。 |
@regression |
合并 main 前,完整覆盖。 |
@slow |
夜间或计划运行。 |
CI 运行标签子集
PR 检查使用原文命令参数:run --expose grepTags="@smoke @critical",grepFilterSpecs=true,grepOmitFiltered=true。合并前,排除已知慢测试:run --expose grepTags="-@slow",grepFilterSpecs=true。夜间运行 run,执行全部。以上是原文 CypressCommandTabs 的命令参数,按所用包管理器调用 Cypress。
关键选项 grepFilterSpecs
没有它,Cypress 先加载编译每个 spec,再过滤。80 个 spec 只有 5 个有冒烟测试,也会全部预处理。设为 true 后,先读取文件找匹配,只加载编译相关 spec。大型套件冒烟测试只覆盖 10—15% 时,可省下其余 85—90% 文件的预处理成本。grepOmitFiltered: true 则让不匹配测试完全跳过,而非标为 pending,输出更整洁。
选择性运行的取舍
Cloud 跨分支比较在测试集合相同时最有意义。分层前要考虑:Branch Review 中,PR 只跑冒烟而 main 跑全部,未选中的测试显示缺失,并非通过;Test Replay 中,被过滤的测试不运行,不生成回放,其失败只能在全量运行时暴露;UI Coverage 的分数来自运行集合,冒烟 PR 与全量夜间相比看似覆盖回退,实际上只是范围不同;Accessibility 同样如此,两边范围一致才适合比较。
建议子集用于快速预检查,同时至少保留一次完整套件运行,例如合并 main 或夜间,为比较功能提供数据。用运行标签或独立 Cloud 项目区分全量与过滤运行,保持仪表盘比较含义准确。
CI 缓存 Cypress 二进制
安装包括 npm 包与 postinstall 单独下载的平台二进制;二进制超过 100MB,每次重新下载是纯开销。CI 作业通常从空环境开始。以 lock 文件作为缓存键,就只在升级时重新下载。
缓存哪些内容
| 路径 | 内容 | 失效条件 |
|---|---|---|
~/.cache/Cypress |
Linux 二进制 | lock 文件变化 |
~/Library/Caches/Cypress |
macOS 二进制 | lock 文件变化 |
~/.npm |
npm 包缓存 | lock 文件变化 |
~/.cache/yarn |
Yarn 包缓存 | lock 文件变化 |
不要直接缓存 node_modules:它绕过 npm/Yarn 完整性检查,命中缓存并跳过 postinstall 还可能造成 Cypress 二进制未下载。缓存包管理器自己的目录,再用 npm ci 或 yarn install --frozen-lockfile 重建。
GitHub Action 自动处理
Cypress GitHub Action自动以 lock 文件为键缓存 npm 与 Cypress 目录,无需额外配置:
- name: Cypress run
uses: cypress-io/github-action@v7
with:
build: npm run build
start: npm start
并行作业使用独立安装作业,让 worker 共享缓存安装,避免各自下载:
jobs:
install:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v7
- uses: cypress-io/github-action@v7
with:
runTests: false
build: npm run build
cypress-run:
runs-on: ubuntu-24.04
needs: install
steps:
- uses: actions/checkout@v7
- uses: cypress-io/github-action@v7
with:
start: npm start
其他提供商
CircleCI 的官方 Cypress Orb可自动缓存,也可用 ~/.cache 手动配置。没有专用 action 的服务,设置 CYPRESS_CACHE_FOLDER 为项目相对路径,让 CI 缓存配置能够访问:
variables:
npm_config_cache: '$CI_PROJECT_DIR/.npm'
CYPRESS_CACHE_FOLDER: '$CI_PROJECT_DIR/cache/Cypress'
cache:
key: $CI_COMMIT_REF_SLUG
paths:
- .npm/
- cache/Cypress
install:
script:
- npm ci
更多信息见 CI 缓存。
记录到 Cloud,持续观察
记录运行可获得系统优化所需分析:测试时长趋势显示哪些测试在变慢;不稳定检测揭示隐藏的重试循环;机器利用情况显示并行分布是否均衡、哪台成为瓶颈。
另请参阅
原文还列出最佳实践、重试能力、@cypress/grep、cy.session、cy.prompt、测试重试、不稳定测试管理、Test Replay、CI 缓存、内存与 CPU 日志、并行化、spec 优先级、自动取消与 UI Coverage 减少重复。
原作:Optimizing test performance,Cypress 文档贡献者。依据官方仓库 MIT 许可证转为中文;完整许可随稿附在 LICENSE-MIT.txt,代码保持原文。这里的耗时、性能范围与命令输出来自原文,没有在本机运行测试验证。原文中的 MDX 产品、命令和配图组件转换为普通文字与配图方案,未制作或声称有真实 Cloud 截图。











暂无评论内容