优化测试性能

大多数性能问题都落在几个类别里:测试类型选错、反复登录的开销、缓慢的真实网络调用、臃肿的 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()'

优先级清单

按下面顺序优化,通常收益最大的项目在前:

  1. 升级到最新 Cypress。
  2. 选择正确测试层次;将 E2E 移到组件或 API 层,往往收益最大。
  3. 用 cy.session() 缓存身份验证,原文估计每个测试节省 2—5 秒。
  4. 用网络桩,把 100—2000ms 的真实调用替换为低于 20ms 的响应。
  5. 程序化准备状态,把数分钟 UI 导航替换为数秒 API 调用。
  6. 并行化;原文将其描述为随机器数量线性扩展。
  7. spec 优先级与自动取消,尽早暴露失败,降低失败构建成本。
  8. 关闭视频并使用 Test Replay,减少每个 spec 的编码开销。
  9. 用 @cypress/grep 标签分层执行 CI;每次 PR 运行快速冒烟或关键测试,grepFilterSpecs 不加载没有匹配测试的 spec。
  10. 缓存 Cypress 二进制,避免每次下载 100MB 以上的数据。
  11. 给 CI 足够资源;资源不足往往表现为缓慢或不稳定,应先解决再调整其他 CI 配置。
  12. 去除任意时长的 cy.wait()。
  13. 精简 support 文件和导入,避免每次 spec 启动都打包无用代码。
  14. 用 blockHosts 屏蔽分析、监控等第三方请求,屏蔽后立即返回。
  15. 避免通配符 cy.intercept('*'),只拦截测试需要的请求。
  16. 使用明确 DOM 选择器,避免 *、div 先收集全部节点再过滤。
  17. 使用现代可见性算法;Cypress 16 默认采用较快的 checkVisibility() 方案,复杂 DOM 尤其受益。
  18. 谨慎重试,runMode: 1 可缓冲不稳定失败,次数应低,并用不稳定数据修复根因。
  19. 用 UI Coverage 找到被重复测试的元素。
  20. 用 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 截图。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容