
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/checkoutv4 - 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?idexample-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 调整保留期和归档策略如果平台提供数据保留期设置建议按业务重要性区分。核心页面和发布版本使用较长保留期内部临时