Chromatic 的费用一旦随着组件库增长,很容易成为每月固定开支里最不起眼又涨得最快的一项。我在一个多包前端仓库中负责视觉回归测试,早期做法很直接:每次提交都跑一次 Chromatic,把全量 Storybook 快照都传给平台,再由平台反馈 UI 差异。组件变多、变体变多、PR 频繁之后,账单比预想中涨得更快。后来我用三个多星期做了一轮成本优化,把月消耗压到原来的 1/10 左右。这个结果并不依赖 Chromatic 的某个特殊功能,核心是控制快照数量、控制构建触发条件、把不必要的工作移到本地,这些做法适用于大多数以快照或构建次数计费的视觉测试工具。
如果只讲结论,很多人会理解为“少测一点”。真实情况不是少测,而是把测试资源放在更有区分度的位置。要解释清楚这一点,需要先理解视觉测试的费用来自哪里,再按顺序处理触发策略、快照数量、本地验证和账号治理。
1. 视觉测试的费用来自哪些维度
视觉测试工具的核心价值,是把代码变化变成可对比的截图。只要组件或页面的渲染结果发生变化,测试平台就会生成新的快照,再与基线对比,告诉开发者哪里出现了偏差。这个流程本身会消耗资源,工具按用量收费时,费用就会与两个东西直接相关:执行的构建次数,以及在每次构建中生成的快照数量。
先看构建次数。同一个仓库如果每次 push 都触发一次视觉测试,那么一个每天有 30 次提交的团队,就会产生 30 次构建。每次构建即使只包含少量快照,也会累计出不小的月度用量。再叠加多个分支并行、多个 Storybook 项目、多个测试模块,构建量会成倍增加。
再看快照数量。快照数量通常与组件数量、组件变体数量、测试文件数量直接相关。一个按钮组件如果定义了 6 种颜色、3 种尺寸、4 种状态,理论上可以组合出 72 个快照。很多视觉测试工具不会自动去重,即使两个快照在视觉上几乎没有差别,它也会当作两个独立快照处理。
费用还会受到其他维度影响。比较常见的有:
| 维度 | 对成本的影响 | 日常容易忽略的地方 |
|---|---|---|
| 构建次数 | 每次运行时都会消耗额度 | 全量跑所有分支、所有提交 |
| 快照数量 | 单个构建中截图越多,消耗越大 | 同一组件存在大量重复变体 |
| 存储与保留 | 历史快照、回归报告长期占用存储 | 从未清理旧版本和过期分支构建 |
| 用户席位 | 按账号数量计费,账号越多基础费用越高 | 离职成员账号未移除 |
| 并行与加速 | 高级并行能力可能单独计费 | 无关紧要的模块也开启高速并行 |
很多团队在引入视觉测试工具时,只关注“快照数量”这个维度,忽略构建次数。事实上,如果某个 PR 只改动了一个工具函数的内部逻辑,并没有影响任何 UI,完全没必要触发一次视觉测试。工具函数发生变化时,组件渲染结果不会改变,生成的全量快照也必然全部通过,但这些快照的构建成本已经被消耗了。
理解这一点之后,优化思路就清晰了:先通过审计弄清楚当前的钱花在哪些构建和哪些快照上,再分头压缩这两个核心指标。压缩快照数量是为了让单次构建更便宜,压缩构建次数是为了减少固定消耗。两条线同时做,才能产生倍率级别的影响。
1.1 把“每次提交都测”改成“每次提交都能测但只在必要时测”
很多视觉测试平台都提供跳过机制,例如通过 CLI 参数、commit message、环境变量来跳过某次构建。这种机制不是为了偷懒,而是为了把有限的预算留给真正需要审查的变化。在没有配置任何条件时,平台只能默认每次提交都跑,因为平台不知道这次提交是否真的触碰了 UI。
合理的做法是在 CI 中设置“默认不跑,满足条件才跑”。条件可以是:路径变更、分支类型、PR 是否标记为 review、commit message 中是否包含[skip visual]。设置完成后,普通依赖升级、文档补全、代码注释修改这一类提交就不会再消耗视觉测试额度。
1.2 用变更范围控制快照生成,而不是生成后再删除
快照数量的压缩应该在构建之前完成,而不是让工具先生成几百个快照再手动删除。这意味着要把 Storybook 的故事配置文件整理成更可控的结构。冗余快照如果只是通过工具后台删除,并不会从根本上减少构建成本,因为构建已经发生了。
从成本角度看,最有效的控制点有三个:代码提交时、CI 作业运行时、工具构建前。在代码提交阶段去掉不必要的配置,在 CI 阶段过滤掉不相关的目录,在工具构建前通过启动参数限制要捕获的故事范围。这三个控制点可以叠加使用。
2. 成本审计:先搞清楚钱到底花在哪
不要凭感觉判断。我做的第一件事是打开项目的使用面板,把近三个月的构建记录导出来,再做一次静态统计。审计目标只有一个:找出哪些构建和哪些快照是必要的,哪些是因为“默认配置”造成的浪费。
2.1 统计仓库里的测试文件数量
先看看仓库里有多少视觉测试入口。常见的是.stories.前缀文件,如果项目使用 Storybook,那么每个 stories 文件中的每个export const都可能被识别成一个 story,最终生成对应的快照。
# 统计项目中的 stories 文件数量,排除 node_modules 和构建产物 find . -path ./node_modules -prune -o -type f \( -name "*.stories.ts" -o -name "*.stories.tsx" -o -name "*.stories.js" -o -name "*.stories.jsx" \) -print | wc -l如果项目里同时存在*.visual.test.ts、*.regression.test.ts这类文件,也需要统计进去。统计结果只是一个粗略基数,真正有价值的是把 stories 文件和组件对应起来,看看哪些组件拥有远超实际需要的 stories 数量。
对于较大的仓库,这种统计用 shell 就够了。如果 stories 文件数量超过几百个,建议加入轻量级脚本,读取每个文件中的export const数量,输出 Top 10 最“重”的测试文件。
for f in $(find src -name "*.stories.*"); do count=$(grep -c "^export const" "$f") echo "$count $f" done | sort -rn | head -10这段脚本的价值并不在于精确统计,而在于快速定位那些一个文件里塞了几十个 stories 的组件。一个组件在视觉上只有几种稳定形态时,通常会生成超过实际需要的快照。
2.2 检查 CI 中的视觉测试触发条件
视觉测试成本往往被 CI 配置决定。打开 CI 工作流,找到视觉测试作业,重点确认三件事:哪些事件会触发、哪些路径会触发、是否有跳过机制。
常见的高成本配置如下:
| 配置 | 高成本表现 | 低成本目标 |
|---|---|---|
| 触发事件 | push、pull_request、schedule 同时触发 | 只保留 pull_request 和主分支 push |
| 路径过滤 | 没有 paths,所有文件变更都触发 | 只有 src、.storybook 相关路径变更才触发 |
| 分支范围 | 所有分支都跑 | 主分支和 PR 分支跑,临时分支不跑 |
| 跳过机制 | 未配置 | commit message 或 label 可以跳过 |
CI 日志里还可以查看每次构建的快照数量变化。如果某一次提交只改了一个 README,却生成了和上一轮一样多的快照,说明触发条件没有正确过滤。
2.3 输出一份成本审计表
审计完成后,建议整理成一份表格,方便后续看到底哪一类消耗最重。
| 项目 | 数量 | 是否可压缩 | 压缩方式 |
|---|---|---|---|
| Storybook 项目数 | 3 | 可合并 | 合并成一个 or 按模块分目录 |
| Stories 文件数 | 420 | 可压缩 | 删除重复变体 |
| 单次构建快照总数 | 2400 | 重点压缩 | 减少 stories 数量 |
| 每月触发次数 | 310 | 重点压缩 | 路径过滤和事件收敛 |
| 历史保留月份 | 6 | 可调整 | 按业务需求设置保留期 |
| 使用人数 | 8 | 可清理 | 移除离职成员 |
这张表不需要列得非常重。关键是找出“快照总数”和“每月触发次数”这两个数值之间的乘积,因为它决定了大部分成本。与其盯着单个变量,不如把这两个变量一起看。优化后可能存在两种结果:单次快照减少,但触发次数没变;触发次数降低,但单次快照数量依然很多。只有两者同步下降,费用才会出现量级变化。
3. 把构建次数降下来:用条件触发代替默认全量跑
调整 CI 触发是成本优化的第一杠杆,也是最不动代码的方式。视觉测试之前对“每次提交都跑”的依赖,很多来自对基线的恐惧,担心漏掉回归。实际上,可以设计一套分层触发机制:核心组件改变时跑完整测试,边缘组件改变时只跑相关模块,完全不影响 UI 的提交直接跳过。
3.1 先从全量构建改成路径过滤
在 GitHub Actions 中,路径过滤可以直接写在事件定义里。以下配置表示只有src目录、.storybook目录或依赖清单发生变化时,才启动视觉测试作业。
name: visual-regression on: pull_request: paths: - src/** - .storybook/** - package.json - pnpm-lock.yaml - yarn.lock push: branches: [ main ] paths: - src/** - .storybook/**这里有一个常见误区:只过滤一级目录是不够的。如果组件 A 的样式依赖组件 B,而 PR 只修改了组件 B 的样式,但视觉测试作业只看src/components/A/**的路径变更,那么测试根本不会触发。优化时要留出公共目录和依赖清单的过滤项。
3.2 在多模块仓库中按模块拆分配置
如果仓库包含多个应用或组件包,可以在 CI 中拆成多个视觉测试作业。每个作业只负责一个模块,并只在该模块路径变化时运行。这样做的成本收益来自矩阵乘积:模块数量如果为 3,之前每次提交跑 3 个作业,现在可能只有 1 个作业会运行。
下面是一个简化的 GitHub Actions 示例,使用paths配合矩阵动态决定哪个模块需要跑:
jobs: chromatic: runs-on: ubuntu-latest strategy: matrix: module: [components, admin, site] steps: - uses: actions/checkout@v4 - if: contains(github.event.pull_request.changed_files, matrix.module) run: echo "run visual tests for ${{ matrix.module }}"如果使用这种矩阵方式,要注意计算一次changed_files的复杂度。大型仓库的 PR 可能涉及几十个文件,contains 判断会随 PR 增大而变慢,但仍然比全量触发便宜。实际落地时也可以直接在 bash 中判断目录是否存在变更,再由脚本决定是否继续。
3.3 使用视觉测试工具自带的跳过能力
Chromatic 这样的工具通常会在 CLI 中提供跳过构建或只跑变更文件的选项。具体参数名会随版本变化,使用前要查看当前安装版本的帮助文档。
npx chromatic --help从社区常见的配置来看,会有--skip、--only-changed、--exit-zero-on-changes这类与构建行为和退出码相关的选项。这里不展开特定版本参数,因为工具更新很快。落地时应该固定 CI 中使用的 Chromatic CLI 版本,避免参数变化导致意外行为。
也有团队通过 commit message 控制跳过。例如提交信息里写[skip visual]时,CI 作业直接退出。这只是减少触发次数的补充手段,能处理一些路径过滤覆盖不到的场景。
3.4 区分基线分支和普通分支
视觉测试需要和基线对比。主分支的每次 push 会产生新基线,这个行为是必要的,但成本也不低。可以降低主分支构建频率,比如只在 release 分支或每天固定时间更新一次基线,而不是每次 push 到主分支都更新。
不过这样做的风险是,基线可能不包含最近提交,导致 PR 对比时出现大量基线差异。更稳妥的做法是保留主分支 push 触发,但通过路径过滤让主分支的构建量远低于 PR 构建量。主分支构建只覆盖部署实际使用的打包结果,普通分支构建只覆盖变更差异,两者相互补充。
4. 压缩快照数量,让单次构建更便宜
构建次数降低之后,下一步要处理的是快照数量。这是成本优化的第二杠杆,也是更精细的工作。快照数量统计起来很直接,但压缩起来需要理解每个测试文件在设计时是否真的需要这么多变体。
4.1 合并视觉上重复的 stories
在一个组件库中,同一个组件增加新变体的成本很低,但视觉测试平台不会自动识别“这两个变体其实长一样”。常见例子是按钮组件定义了Primary、Secondary、Ghost、Link,其中Ghost和Link在视觉上可能只差一个边框。如果这两个形态在开发调试中有必要,但并没有出现在任何真实业务页面中,就没有必要把它们放进视觉测试快照里。
推荐做法是把 stories 分层:
| 层级 | 用途 | 是否进视觉测试 |
|---|---|---|
| 开发 story | 方便本地调试组件细节 | 否 |
| 展示 story | 覆盖真实业务中的关键视觉状态 | 是 |
| 文档 story | 用于文档展示,不承担回归价值 | 否 |
在 Storybook 中的实现方式不是固定的。可以直接在 stories 文件里控制导出,也可以给 story 添加 tag 或参数,再由 Chromatic 插件在收集时过滤。对于 Chromatic,具体的过滤方式依赖插件配置,但更通用、更不容易出错的方法是控制 stories 文件本身的导出数量。
4.2 把交互逻辑测试从视觉测试中剥离
很多组件快照承担了不必要的任务:它们不仅是视觉快照,还隐含着对渲染逻辑的验证。比如一个 Tooltip 组件,open状态、hover状态、click状态在视觉上几乎相同,却被定义成多个 stories。这类交互状态更适合用组件测试工具验证,而不是用视觉测试工具。
组件测试和视觉测试的分工是:组件测试验证行为,视觉测试验证外观。一个弹窗组件,应当用 Jest 或 Testing Library 验证“点击后出现内容”“焦点移动正确”,再把“默认关闭状态”“打开状态”的视觉快照交给视觉测试平台。这样既保住了行为安全,又减少了一半以上的快照。
4.3 使用固定数据与装饰器减少重复
视觉快照出现大量冗余,还有一个常见原因是装饰器把同样的包裹层渲染了一遍。比如每个组件 story 都套了一个ThemeProvider,同一布局容器在每次快照里都渲染一次。这不是测试文件自身的问题,而是视觉测试天然会把完整页面截图,所以任何重复的装饰节点都会镜像在截图里。
可以通过设置 Storybook 的全局 decorator 减少每个 story 单独写包装的比例。真正的优化点在于:一个组件如果需要 10 个 story 展示不同状态,可以使用同一个 decorator 渲染同一个容器。这样虽然每个 story 在截图上仍然包含容器,但至少不会因为代码重复导致某个格式变化时产生 10 个分支快照。
4.4 调整截图尺寸与浏览器矩阵
截图尺寸和浏览器覆盖范围同样影响快照数量。如果每个 story 默认在三种浏览器、两种尺寸下截图,数量会立刻乘以 6。大多数视觉工具默认可能不是每种尺寸都截,但团队有时会为了跨浏览器兼容增加覆盖范围。针对已经进入稳定的组件,可以在保留截图数量不变的情况下,把跨浏览器矩阵限定在真实业务使用的浏览器上。
以下是一个常见的取舍逻辑:核心登录页、用户主页、组件库设计规范页面保留多浏览器覆盖,内部管理组件的弹窗、表单等局部组件只保留单浏览器、单一尺寸。这样能够把快照数量压低,同时保证高价值页面仍然有跨浏览器保护。
5. 把不必要的测试从付费平台移到本地
视觉测试平台的计费方式决定了,只有真正需要人工评审和团队协作的构建才值得把数据上传到平台。对于开发过程中的快速反馈,完全可以在本地或普通 CI 中用开源测试工具完成。这样做的收益不是减少测试,而是减少“需要上传到平台的快照”数量。
5.1 用 Playwright 做本地语义快照
Playwright 本身可以把页面或 iframe 截图,也可以把截图与基线截图做对比。在 Storybook 场景下,可以启动本地 Storybook,再访问iframe.html中对应 story 的地址进行截图。
以下是一个简化示例,仅用于说明思路。实际项目需要根据 Storybook 版本、端口和 story id 规则调整:
import { test, expect } from '@playwright/test'; const url = 'http://localhost:6006/iframe.html?id=example-button--primary'; test('button primary snapshot', async ({ page }) => { await page.goto(url); const screenshot = await page.screenshot(); expect(screenshot).toMatchSnapshot('button-primary.png'); });这段脚本在本地运行时能快速发现样式和布局有明显变化的问题。它不依赖视觉测试平台,也不会消耗平台额度。对于开发者分支、草稿 PR、未进入评审的开发分支,这种检查已经足够。
另一个思路是在 CI 中运行 Playwright 视觉测试,但只在 Chromatic 构建之前作为“快速冒烟层”。如果本地快速检查发现大量差异,CI 直接失败,提前提示开发者修正,再决定是否触发 Chromatic。
5.2 哪些内容仍然应该交给 Chromatic
并不是所有测试都适合全部搬到本地。跨浏览器截图、多人评审、分支合并时的基线对比、UI review 时逐一点击查看控件变化,这些能力是视觉测试平台的核心价值。如果本地脚本无法覆盖真正的跨浏览器差异,把核心交互流程留在平台上更合理。
我在实际执行时,把测试分成了三层:
| 层 | 内容 | 运行方式 |
|---|---|---|
| 本地冒烟 | 高频开发的组件、日常样式调整 | Playwright 本地截图对比 |
| 普通 CI 视觉测试 | 公共组件、核心页面、主题变更 | Chromatic 或托管工具按路径触发 |
| 发布前人工评审 | 跨浏览器、跨设备、正式回归 | 保留在平台上并设置专门 baseline |
这样分层的直接效果是:平台上跑的构建少了,但每个构建覆盖的内容更接近真实用户价值。开发者在本地也能获得足够反馈,而不是所有事情都依赖平台。
5.3 注意本地和平台的基线一致性
本地测试容易出现一个问题:本地环境与 CI/平台环境不一致,导致本地截图不断出现差异。常见原因包括字体加载、系统渲染、时间线变化、图标字体缺失。解决方法是尽量在 Docker 容器中运行本地截图,并固定基础镜像,与 CI 使用相同的操作系统和浏览器版本。
视觉测试的稳定性取决于可控环境。如果本地 Playwright 跑出来的结果每天都在变,那么这个本地测试就没有拦截能力,只会被开发者视为干扰。为了减少这种干扰,可以在本地脚本中禁用动画、固定视口、注入稳定的系统字体,设计出一个可重复的基线。
6. 账号与保留策略,减少不必要的固定支出
视觉测试工具的费用不只是由构建和快照决定。长期不用的项目、已离职成员的账号、过长的数据保留期,都会提升固定支出。这类成本不产生任何测试价值,但清理起来相对简单。
6.1 定期清理项目和分支
一个组织内可能有多个仓库接入同一个视觉测试平台账号。新仓库接入时通常会创建一个新项目,但仓库很久不再有活跃 UI 更新后,项目依然保留在平台中,并产生存储费用。可以按季度整理一次项目列表,把连续 30 天没有构建的项目归档或删除。
同时要处理分支产生的历史构建。一些工具会为每个分支保留多份历史快照,用于后续分支对比。如果开发流程中只使用main和少量 long-lived 分支,可以删除已合并分支的旧构建,或关闭相关分支的自动保留策略。
6.2 合并多个 Storybook 入口
一个仓库如果有多个 Storybook,在平台上通常对应多个项目。多个 Storybook 入口会带来重复装饰器和重复组件,导致同一组件在多个项目中被重复截图。让所有模块共享一个统一的 Storybook,再通过配置只暴露对应模块的 stories,可以显著降低重复构建。
这不是简单的“项目合并”。如果两个 Storybook 使用完全不同的主题或全局配置,合并后可能会改变截图内容。操作前需要对比两个配置的差异,调整 decorator 与 providers,确保合并后的视觉基线仍然有效。
6.3 调整保留期和归档策略
如果平台提供数据保留期设置,建议按业务重要性区分。核心页面和发布版本使用较长保留期,内部临时