Chromatic 这类视觉测试工具的账单,最容易被忽略的一点是:它不是按项目个数收费,而是按“快照消耗量”收费。你的代码库可能只有几十个组件,但每个组件在不同浏览器、不同视口下都生成快照,再加上每次提交都跑一遍云端全量,月底的快照数就会非常难看。这篇文章我来讲一套从“全量快照”改成“有效快照”的优化思路。做法不绑定 Chromatic 特有接口,Percy、Argos、Applitools 这类工具同样能套用。如果你的项目属于多组件、多浏览器、多视口、PR 频繁的类型,把快照消耗降到原来的十分之一并不夸张。
很多人觉得视觉测试省钱就是把并发调低、少开几个 CI 任务,实际上账单大头通常来自测试覆盖范围本身。同样一段代码,多配一种浏览器就是多一倍快照;多配一个视口又可能多一倍快照;每个 PR 都全量跑一次,整体消耗还会按提交次数继续放大。与其到最后去处理账单异常,不如先从快照生成逻辑上把不必要的内容按掉。
下面我按实际落地顺序拆一遍,先判断问题在哪,再动手改配置,最后做长期约束。
1. 先搞清楚视觉测试账单是被什么撑大的
1.1 计费逻辑不是按项目数,而是按“快照消耗量”
Chromatic 和同类视觉测试平台,通常不是只按项目数量或成员数量收费,真正决定费用增长的是每次测试产生的快照数量。这里的快照,可以简单理解成“一张被上传到云端做对比的截图”。一个组件 Story 只要进入测试集,就可能对应一张或多张快照;如果配置了多个浏览器、多个视口,快照数量还会成倍增加。
我经常看到这样的项目:功能代码不算复杂,但 Storybook 里每个组件都写了七八个 Story,每个 Story 又配了桌面端、平板端、手机端三种视口,云平台那边再开 Chrome、Firefox、Safari 三种浏览器。这样算下来,一个组件可能产生几十张快照。组件库只要达到几十个组件,一次云测试就是上千张快照。这个量级在免费额度里可能看不出问题,一旦团队进入正常开发节奏,每次 PR 都触发一次全量测试,账单增长会非常直接。
还要注意“重复跑”的问题。很多平台不仅会捕获正常结果,重试、超时后的再次抓取,也可能被计入快照使用量。批量告警、CI 网络抖动、资源不足导致的截图失败,都会让同一份代码被反复上传比较。这类消耗和代码覆盖没有关系,纯粹是流程问题,但它对账单的影响可能比想象中大。
所以第一步不是急着删 Story,而是先搞清楚你当前一个 build 到底生成了多少快照,以及这些快照分别来自哪里。
1.2 默认配置会放大消耗的几个位置
大多数视觉测试工具接入 Storybook 之后,默认行为都是“尽量多测”。这符合工具本身的目标,但不一定符合账单目标。以下位置特别容易放大消耗。
第一,Story 全量进入测试集。只要 Storybook 能识别到的 Story,默认都会被云平台抓取。哪怕只是一个验证交互逻辑的辅助 Story,也会被当成视觉回归用例处理。
第二,视口配置被复制到所有 Story。很多团队在 Storybook 全局配置里设置了多个 viewport,例如 iPhone SE、iPhone 14、iPad、桌面 1280、桌面 1920。全局视口一多,每个 Story 都会按这些尺寸各生成一张快照。实际业务里,并不是每个组件都需要在五种尺寸下检查。
第三,浏览器矩阵过宽。Chromatic 这类工具可以配置 Chrome、Firefox、Safari、Edge 等浏览器。浏览器越多,快照倍数越高。很多组件并不涉及浏览器差异,却为每个浏览器付了一遍快照成本。
第四,每次提交都触发全量回归。如果 CI 里只要 push 就运行云端视觉测试,那么一个分支每天的提交次数会直接把快照消耗拉到很高。团队在功能分支上反复调试样式时,每调整一次都会跑一整轮全量快照,其中大部分组件完全没有变化。
第五,交互状态也被当成快照点。一些工具支持在 Story 的 play 函数里模拟点击、悬浮、输入操作,操作后的状态也会被捕获。这类测试很有价值,但成本也高。一个 Story 里有三次交互,就相当于三个或更多测试点。如果这些交互状态并不是你需要长期守护的核心样式,就不该每次都在云端跑。
把这几类因素放在一起,就能看出账单高的原因通常不是“测试太多”,而是“无效快照太多”。
2. 动手前先盘点当前项目,建立减少快照的基线
2.1 先找出所有快照入口
不要凭感觉判断哪些组件消耗多。先拉取一份实际快照清单,或者根据 Storybook 的索引文件做一个估算。
最简单的做法是先在本地跑一次 Storybook 的静态构建,然后找到生成的index.json或stories.json文件。这类文件会列出所有 Story 的 title、id、importPath 等信息。用一个临时脚本按 title 分组统计,就能知道每个组件下面有多少个 Story。路径以你项目实际输出为准,不同 Storybook 版本文件名可能不同。
下面是一个通用的统计参考脚本,不是固定命令,需要根据你的构建产物结构调整:
# 示例脚本:统计 Storybook 索引文件里的 Story 数量 node -e " const data = require('./storybook-static/index.json'); const map = {}; for (const [id, item] of Object.entries(data.entries || {})) { const comp = item.title || 'unknown'; map[comp] = (map[comp] || 0) + 1; } console.table(map); "如果你用的平台提供了快照报告,也可以直接打开最近一次云测试结果,按组件或 Story 维度看快照数量。有些平台还支持导出 CSV,这种数据比本地估算更准确。
这一步的核心目的,是形成一份“快照消耗基线”。不要边改边看效果,因为缺少基线的话,你不知道改动后到底省了多少,也不知道是否有组件因为误删而完全失去视觉守护。基线里至少要记录三组数据:当前 Story 总数、当前浏览器数量、当前视口数量,以及最近一次全量测试的快照总量。
2.2 按组件变更频率给测试分级
拿到 Story 清单后,不要直接开始删,先把组件按价值和变更频率分级。我一般会分成三档。
高优先级:公共组件、设计系统核心组件、用户主流程里最容易出现样式回归的组件。比如 Button、Input、Table、Modal、Navbar。这类组件要保留足够的视觉覆盖,浏览器和视口可以适当保留两到三个关键组合。
中优先级:业务页面、业务组件,只对用户可见的关键状态做视觉测试。比如页面加载态、空态、错误态、主要数据态。不需要把所有数据组合都变成 Story。
低优先级:很少变动的静态页面、营销页面、纯文档类组件、长期无人维护的旧页面。这些可以在合并到主干时跑一次,而不是每个 PR 都跑。
做完分级后,重新看一遍“快照消耗基线”。你会发现,真正值得每次 PR 都跑全量快照的组件,可能只占整个项目的一半甚至更少。其余部分应该靠版本发布前的全量回归来覆盖。
这个分级表可以直接写在 README 或项目文档里,后续新 Story 进来时按同一套标准评审。否则今天省下来的额度,过两个星期又会因为随手新增 Story 涨回去。
3. 用配置和代码按层级削减快照数量
3.1 从 Story 层过滤低价值用例
Story 是视觉测试的最小入口。控制 Story 数量,是最直接的削减方式。但不是让所有 Story 都消失,而是把“不必要的 Story”和“有必要的 Story”区分开。
如果你发现某个组件下面有二十多个 Story,大部分只是参数组合不同,就应该考虑合并。一个视觉状态只需要保留一个最有代表性的 Story。比如 Badge 组件,用“默认”“成功”“警告”“危险”四个状态就能覆盖主要样式,不必再为每个状态多写一个“带图标”的 Story。带图标的变体可以在其中一个代表性 Story 里用参数组合展示,不必为每一种图标单独建 Story。
另一个容易被忽略的点是:有些 Story 存在的目的只是为了验证组件可交互,并不关心它的最终截图效果。这类 Story 可以不打入视觉测试集。很多视觉测试平台支持在 Story 或者组件级别做跳过标记,不同版本和平台叫法不同,常见的是 skip、disable、exclude 这类配置。具体字段以你当前使用的工具文档为准,但判断标准是一样的:如果一个 Story 的截图变化不会帮助你发现回归,就不需要让它进入云端对比。
我一般会在内部定一个规则:新建 Story 时,先问自己一个问题——“如果这个 Story 的截图发生了样式变化,我会不会认真来看?”如果答案是不会,就说明这个 Story 不适合放进去。
3.2 用视口、浏览器和交互参数做减法
很多项目的视觉测试成本高,不是因为 Story 多,而是因为每个 Story 都在多浏览器、多视口下重复截图。这部分的优化收益最大,也最容易操作。
先看视口。对大多数组件来说,常见的检查尺寸可以收敛为两种:移动端 390 或 414,桌面端 1280 或 1440。平板端通常只在抽屉、弹窗、栅格布局这类确实会改变排列方式的组件上检查。不要把五个视口全部挂到全局,最好改成一个基础视口列表,再让个别组件单独补充需要的尺寸。
再看浏览器。视觉回归测试最核心的价值是防止“代码改动导致布局或样式变化”,并不一定要在所有浏览器里都重复验证。如果项目使用现代 CSS 标准,并且主要用户集中在 Chrome 内核浏览器,可以先把浏览器配置收敛为默认浏览器,最多保留一个移动端浏览器。真正需要跨浏览器验证的情况,比如针对 Safari 的兼容补丁、针对 Firefox 的细节差异,再单独给相关组件开一个浏览器矩阵。这样既保留跨浏览器能力,又不会让每个普通 Story 都承担两到三倍快照。
交互参数也要控制。一个 Story 里的交互状态越少,快照点就越少。如果有某个组件需要验证“点击后展示弹窗”“输入后展示错误信息”“悬浮后显示 tooltip”,建议拆成多个独立 Story,而不是一个 Story 里连续做三次操作。拆开之后,你还能更清楚地定位到底是哪个交互状态出了问题。但同时也要注意,拆成多个 Story 不等于每个都必须进视觉测试。确定这几个状态里,哪个才是视觉回归的高风险点,只保留那一个或两个。
3.3 用批处理和任务拆分管理高峰消耗
除了测试维度,触发频率也需要控制。默认情况下,很多项目是“每次 push 都全量跑”。如果是个人项目还好,如果是团队项目,一个功能分支可能同时有十几个人在提交,每天触发几十次全量测试。这个数字比视觉测试自身的配置还要可怕。
先做触发条件收敛。常见思路是:只在目标分支和特定 Pull Request 事件上运行视觉测试,开发分支的中间提交不跑。比如main分支每次合并前跑一次全量,PR 阶段按变更范围跑一次增量。如果属于文档、依赖升级、纯配置调整,可以直接跳过。
再按文件变更范围决定运行级别。只要 CI 能拿到当前分支相对基准分支的变更文件列表,就可以根据路径判断是否需要触发视觉测试,以及需要跑哪些组件。下面是一段通用伪代码:
# 示例:按变更目录决定是否运行视觉测试 CHANGED=$(git diff --name-only "$BASE...$HEAD") if echo "$CHANGED" | grep -qE "^docs/|\.md$"; then echo "skip_visual" elif echo "$CHANGED" | grep -qE "^src/components/"; then echo "run_visual_for_components" else echo "run_visual_default" fi这段脚本的核心不是具体命令,而是建立“开关机制”。拿到结果后,CI 再决定调用哪一个视觉测试命令,是跑全量还是按指定目录过滤。
如果你的项目是 monorepo,最好把视觉测试任务拆到包或应用维度。避免任何一个仓库的任意改动,都触发整个 monorepo 的视觉测试。拆分后,每个视觉测试任务的范围更小,快照数量更可控,失败时也更容易定位。
4. 引入本地验证和差分思维,让 CI 只在必要时消费云端额度
4.1 本地先跑静态检查和组件自测
云端视觉测试的价值在于有历史基线,能自动对比前后差异。但很多样式问题其实不需要云端对比也能发现。本地 Storybook 本身就是很有效的检查工具。
我的经验是:在提交代码前,先本地启动 Storybook,快速人工扫一遍改动组件的几个核心状态。确认布局没有因为重构发生明显偏移,颜色、间距、字体层级没有明显异常,再进入 CI。这一步不消耗云端额度,也不需要写额外测试,但因为工程师每天会接触组件,很多低级回归在本地就能发现。
如果团队对这部分有更高要求,还可以在本地或 CI 里加一条轻量级的“静态检查”。比如用 Playwright 对 Storybook 页面截图,保存到本地临时目录,不需要上传到云端,只是用于快速确认页面能正常渲染、没有白屏、没有明显控制台错误。这类检查不能替代视觉回归测试,但可以拦截掉大部分“启动即失败”的问题,避免云端空跑。
云端视觉测试应该用来解决“自动化对比历史基线”这件事,而不是用来代替工程师开发时的基本检查。把低价值的确认工作放在本地,把高价值的对比工作留给云端,额度消耗会更合理。
4.2 只 push 有变化的配置到云端
有些视觉测试平台支持自动识别“哪些 Story 真正发生了变化”,比如根据构建产物和源码依赖关系做增量分析。类似能力在不同平台叫法不同,Chromatic 里有 TurboSnap 这样的增量方案。思路是一致的:通过依赖分析,只重跑受影响的组件,而不是每次都全量重跑。
如果平台提供了这类能力,优先打开。打开之后,即使你本地全量构建 Storybook,云端也可能只会抓取真正发生变化的 Story。这能让常规 PR 的快照量大幅下降。
如果平台没有增量能力,就需要在 CI 侧自己实现“先判断是否真的要跑”的逻辑。比较稳妥的做法是分成两步:
# 示例:CI 流程拆分 steps: - run: npm install - run: npm run build-storybook - run: node scripts/decide-visual-test.mjs id: decide - run: npx chromatic if: steps.decide.outputs.run == 'yes'这个流程的含义是:先构建本地 Storybook,再根据变更文件决定是否调用 Chromatic 命令。如果决定结果是 skip,就直接跳过云端上传。这样可以保证,只有真正有视觉变化风险的提交,才消耗云端额度。
有些人会觉得“多跑一次 build-storybook”浪费时间,但它用的是 CI 机器资源,通常比云端快照额度便宜得多。对于账单敏感的团队,值得用一点 CI 时间换云端消耗。
4.3 独立构建任务控制并发
很多平台的套餐里,并发数会影响体验,但真正烧额度的是快照总数。如果你观察到某个时间段快照消耗异常高,除了看测试配置,还要看 CI 触发频率是否过高。
我建议把视觉测试从常规单元测试里拆成独立任务。不要让每次 push 都同步跑视觉测试,也不要让多个分支在同一时间并行触发大量全量测试。更好的做法是:
- PR 打开时,只对变更组件做增量视觉测试。
- main 分支合并时或发布前,跑一次完整回归。
- 临时分支、草稿 PR、机器人提交的分支,尽量跳过或手动触发。
这样可以避免一个峰值时段内十几个分支同时向云平台上传快照。对账单最直接的贡献,是减少了正常开发过程中大量重复的全量消耗。同时也能降低平台侧的网络波动和超时重试,间接减少额外快照。
5. 拿一个模拟场景估算效果:为什么能达到 10 倍差距
5.1 估算方法
要判断能不能实现 10 倍下降,不要只看单次优化,要看多个因素的乘积。先设置一个典型场景。
假设项目有 50 个组件,每个组件平均 8 个 Story,总共 400 个 Story。优化前每个 Story 使用 4 个视口、3 种浏览器。那么一次全量测试的基准快照数是:
400 Story × 4 视口 × 3 浏览器 = 4800 张快照。
如果团队每周有 20 次全量测试,月消耗大约是 96000 张快照。即便平台免费额度再高,这个量级也会很快进入付费阶段。
优化分三步:
第一,把大部分组件的浏览器收敛为 1 个,基础视口收敛为 2 个。那么同样 400 个 Story 的全量快照变为:
400 Story × 2 视口 × 1 浏览器 = 800 张快照。
第二,把 PR 阶段的全量测试改为按变更组件过滤。假设一个 PR 平均影响 50 个组件里的 10 个,那么 PR 阶段每次消耗:
10 个组件 × 8 Story × 2 视口 × 1 浏览器 = 160 张快照。
第三,只在合并主干或发布前跑完整回归。如果每个月有 20 次 PR,外加 4 次全量回归,总消耗大约是:
20 × 160 + 4 × 800 = 3200 + 3200 = 6400 张快照。
对比原来的 96000 张快照,节约效果接近 15 倍。即使你的项目没有 50 个组件,只要同时做到了浏览器收敛、视口收敛、按变更跑测试,数量级下降依然明显。
这不是一个精确的官方数字,只是一个用来建立预期感的估算框架。你可以把自己的组件数、Story 数、浏览器数和 PR 次数套进去,算出你的项目理论上能省多少。
5.2 哪些项目最受益
这套优化思路并不是对所有项目都同样有效。如果项目只有 10 个组件,测试量本来就很小,再怎么减可能也就是从 200 张降到 50 张,绝对金额变化不大。但如果你的项目满足以下条件,10 倍优化是现实目标:
- 组件库或页面组件数量超过 50。
- 每个组件下有大量状态 Story。
- 项目里配置了多个浏览器或多个视口。
- CI 对每次 push 都执行全量云端测试。
- 团队有多个开发分支同时推进。
这类项目的快照成本通常不是线性上涨,而是随着组件数量、Story 数量、浏览器数量和触发次数同时上涨。反过来,只要把其中两三个变量压下来,费用就会快速回落。
如果项目只有一个静态展示页,用不上太复杂的策略。把 Story 数量控制在最小必要范围,然后定期跑一次全量就可以。
6. 边界、坑点和长期维护建议
6.1 省额度不等于省测试覆盖率
优化快照数量时,最怕的是为了省钱把关键测试也删了。视觉回归测试的核心价值在于守护那些“不该变化”的样式,如果删掉了,回归风险会上升。尤其是核心组件和关键业务状态,宁可多留一个 Story,也不能因为账单焦虑全部砍掉。
我比较推荐的做法是“分级守护”:日常 PR 只跑核心组件和变更组件的快照,合并到主干前跑全量回归。这样日常开发负担小,但发布前仍然会做一次完整检查。也就是说,省的是“重复验证”的量,不是“关键验证”的量。
另外,视觉测试并不需要覆盖所有 UI 状态。像“数据已加载但有 1000 条记录”“列表为空”“接口报错”这类状态,应该由单元测试或端到端测试去覆盖。视觉测试只需要验证它们渲染出来的样式是否正常,不需要为每种数据长度单独造快照。
6.2 常见误区和排查顺序
优化过程中很容易踩几个坑,我单独列一下。
第一个误区是只改 Story 数量,不改浏览器和视口。Story 减掉 30%,但浏览器仍有 3 个,视口仍有 4 个,最终快照量下降不明显。浏览器和视口是倍数放大器,优先处理这个部分效果更直接。
第二个误区是本地过滤脚本太保守,导致很多本该跑的组件被跳过了。比如脚本只匹配了src/components,但组件实际在packages/ui/src下,那么整个视觉测试都会静默失效。判断标准是:过滤后要能在平台报告里看到对应组件的快照,而不是只剩下一个空测试集。
第三个误区是只关注单次 build,不关注触发频率。单次从 4800 张降到 800 张已经很好,但如果每天仍然触发 20 次全量测试,总的额度消耗还是偏高。触发频率要和单次快照量同时优化。
如果账单突然异常,建议按这个顺序排查:
- 先看最近几天的 build 数量,有没有因为临时分支、重复推送导致大量重复触发。
- 再看单次 build 的快照总量,和之前相比有没有明显增长。
- 查看浏览器和视口配置,确认是否有人为了提高兼容性,把全局矩阵扩大。
- 查看 Story 数量和新增 Story,确认是否有人在日常开发中不断添加低价值 Story。
- 最后看失败重试次数,如果网络或资源问题导致反复抓取,既要解决运行环境,也要优化并发。
这个顺序能帮你快速定位是“流程问题”还是“配置问题”,而不是每次都在账单页面里猜。
6.3 长期维护建议
优化一次并不难,难的是让快照数量不反弹。我把长期维护建议总结成几点。
第一,把 Story 评审写进开发规范。新增 Story 时,如果不是新增视觉状态,就尽量复用已有 Story,或使用参数组合展示。一个组件最多保留多少个视觉 Story,可以写一个参考值,超了就提醒团队先做合并。
第二,用快照预算做 CI 检查。如果你能拿到上一次 build 的快照总数,可以在 CI 脚本里设置一个阈值。当快照数量超过预算时,直接失败或给出警告,让开发者在合入代码前意识到测试集变大了。
第三,周期性清理低价值 Story。每隔一两个迭代,检查一次 Storybook 目录和视觉测试报告。长期没有变更、没有实际业务使用、维护者自己都不确定的 Story,可以标记为跳过或直接删除。
第四,把全量回归固定在发布流程里。日常 PR 可以省,但发布前一定要保证跑一次完整视觉测试。这样可以避免“日常开发时很省,但上线前才发现样式回归”的尴尬。
回到标题里的 10 倍,本质上不是靠某一个魔法开关,而是把 Story 数量、浏览器数量、视口数量、触发频率、增量策略五个方面都做减法。单个因素可能给你带来 1.5 倍到 3 倍的收益,乘在一起才能真正改变账单量级。这种思路并不绑定 Chromatic,换到任何视觉测试工具都成立。关键不是工具叫什么,而是你有没有真正控制住“快照点”这个最核心的成本单位。