关于 bundle-size-optimization
用于衡量并优化 TanStack 相关软件包生成的 JavaScript 包体积的仓库技能。它指导编码 Agent 选择基准场景、比较结果、分析变更并验证,同时关注运行性能与可维护性。
主要功能
- 01通过 Nx 测量生成的包
- 02保存并比较基准快照
- 03检查文件指标与来源分析
- 04运行定向性能基准
- 05验证测试与最终变更
适用场景
- 01将候选版本与已保存基线比较
- 02按场景和文件定位包体积变化
- 03检查优化是否影响运行速度
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ tanstack/ router --skill bundle-size-optimization安装后在 Claude Code 中输入/bundle-size-optimization确认已加载。
详细介绍
Bundle 大小优化
概述
优化测量得到的客户端 Bundle,而不是源文本。权威数据来源是 @benchmarks/bundle-size:measure、benchmarks/bundle-size/results/current.json 和 benchmarks/bundle-size/dist/ 中生成的 JS。
使用 benchmark:bundle-size:run 进行本地迭代。它使用相同的测量脚本,并通过 Nx 构建所选软件包。
命令
| 需求 | 命令 |
|---|---|
| 完整基准测试 | pnpm benchmark:bundle-size:run |
| 保存基线 | pnpm benchmark:bundle-size:run --name baseline --scenario react-router.minimal |
| 测量并比较 | pnpm benchmark:bundle-size:run --baseline baseline --scenario react-router.minimal |
| 保存候选结果 | pnpm benchmark:bundle-size:run --name candidate --baseline baseline --scenario react-router.minimal |
| 测试、测量并比较 | pnpm benchmark:bundle-size:run --baseline baseline --scenario react-router.minimal --test-projects @tanstack/router-core,@tanstack/react-router -- tests/path.test.ts tests/link.test.tsx |
| 查看结果 | pnpm benchmark:bundle-size:query --id react-router.minimal |
| 比较现有结果 | pnpm benchmark:bundle-size:diff --baseline ./baseline.json --id react-router.minimal |
| 历史差异 | git fetch --quiet origin gh-pages && pnpm benchmark:bundle-size:history --id react-router.minimal --top-deltas 20 |
| 收集源码归因信息 | pnpm benchmark:bundle-size:run --scenario react-router.minimal --analysis |
| 查看源码归因信息 | pnpm benchmark:bundle-size:analyze --id react-router.minimal |
| 符号引用 | pnpm ts:symbol-references -- --project packages/router-core/tsconfig.json --file packages/router-core/src/utils.ts --symbol last |
命名运行会将指标和日志保存在 benchmarks/bundle-size/results/runs/<name>/ 中。已有的命名结果无法覆盖。
未指定 --name 时,运行会替换 results/current.json 以及所运行步骤的日志。
--baseline 接受运行名称或 JSON 文件路径。--results-dir <dir> 会更改当前运行和命名运行的结果根目录。
除非通过 --dist-dir 指定其他目录,否则生成的 Bundle 仍会使用 dist/。
对于命名结果,请向查询、差异或分析工具传入 --current benchmarks/bundle-size/results/runs/<name>/current.json。
规则
- 一次只运行一条 Nx 命令。
- 对于环境变量、重定向、退出代码、日志尾部和结果查询,请使用运行器,而不是 Shell 命令链。
- 运行 Agent 时,使用
pnpm --silent benchmark:bundle-size:run ...,以省略包管理器命令回显。 - 运行器会设置
CI=1和NX_DAEMON=false。它会将完整输出保存到current.json所在目录的tests.log和measure.log中。 - 如果某个步骤失败,运行器会停止,并最多打印 40 行日志,且不超过 8,000 个字符。若要了解详情,请查看报告中提到的日志。
- 标准输出安静并不表示 Nx 卡住了。在应用 Nx 重置/重试保护措施前,请先查看报告中的日志,确认进度。
- 对发生变更的包使用
--test-projects。单元测试文件选择器和标志应放在--之后。 - 运行器不会自动选择测试。类型测试、性能基准测试和端到端测试应分开运行。
- 基线和候选方案的场景选择及测量标志必须相同。每个保留的快照都使用新的
--name。 - 包源代码变更后,不要添加
--skip-package-builds。默认行为会通过 Nx 重新构建,并保留有效的包缓存。 - 优先跟踪
gzipBytes;同时检查initialGzipBytes、rawBytes、brotliBytes、jsFiles和逐文件的files。 - dist 路径使用
scenarioDir/outDir,而非指标 ID:react-router.minimal对应dist/react-router-minimal/。 - 对微小变更,应在每个候选方案完成后进行测量;gzip 可能与原始字节数呈相反变化。
- 若要比较某个基线提交,请在单独的工作树中测量相同场景。将保存的
current.json路径传给--baseline。 - 历史记录可用于查找以往模式和基线,不能用于确定源码归属。这是提交级数据。
- 绝不能为了缩减包体积而牺牲运行时性能和安全性。
- 不要在首次验证成功后就停止。应持续尝试合理的本地、输出 JS 和算法层面的候选方案,直到测得的回归、可读性或风险排除剩余方案。
- 内联辅助函数或简化不明显的逻辑时,应添加简短注释说明其含义/不变量,而不是实现机制,以保持可读性。
- 内联或删除辅助函数/函数前,请使用 TypeScript 语言服务脚本检查引用:
pnpm ts:symbol-references -- --project <package>/tsconfig.json --file <decl-file> --symbol <name>。如果辅助函数在其他地方也有使用,通常不值得仅为缩减包体积而内联其中一处,除非测量结果证明值得。如果已无任何引用,请删除辅助函数,并用该脚本验证。 - 对正在修改的包运行单元测试/类型测试,并运行
e2e/下相关的端到端测试。 - 持续优化,直到进一步缩减会让代码难以阅读/维护,或已无可缩减之处。无需达到用户指定的字节数目标。
- 可以进行大幅且有风险的架构或算法变更,但必须遵守上述运行时、安全性、可读性、可维护性和测试约束。
- 如果不确定是否应纳入一项已通过的变更,因为它可能改变运行时语义,请添加单元测试/端到端测试,和/或提出澄清问题。如果可以通过探索代码库找到答案,请先探索。对于每个问题,请给出你建议的答案。
- 如果运行时性能影响不明确,请添加有针对性的 Vitest 基准测试(
*.bench.ts),使用真实场景和边缘场景的输入比较候选实现,例如packages/router-core/tests/closing-tag-detection.bench.ts;在bench()测试用例前,先验证各实现产生的结果完全相同。 - 如果发现可复用的包体积优化模式、遇到工具缺陷,或缺少分析能力,请在更新此技能或基准测试脚本前先征求用户同意。
基准测试规则
- 迭代期间,选择一个最可能包含改动代码的包体积场景。默认情况下,router-core 和 react-router 的改动使用
react-router.minimal;solid-router 的改动使用solid-router.minimal,vue-router 的改动使用vue-router.minimal,React Start 的改动使用react-start.minimal或react-start.rsbuild.minimal,Solid Start 的改动使用solid-start.minimal。 - 如果代码只会被纳入更完整的场景,则覆盖默认目标场景。例如,如果某个钩子/函数只在
solid-router.full中引用,就对solid-router.full进行迭代,而不是solid-router.minimal。 - 如果改动可能影响多个包系列,先选择会导入共享代码的最小场景,以便快速迭代;然后在最终确定前,对下一个最可能受影响的系列进行抽查。
- 最终确定前,不带
--scenario运行完整的包体积基准测试,并比较所有场景。即使目标场景有所改善,也要留意异常值/异常情况。 - 直接对改动所涉及的机制进行基准测试,而不只是测试其公共 API。
- 将覆盖面广且贴近实际的场景用作冒烟/回归测试;用聚焦的用例提供依据。
- 使用相同的基准测试文件比较基线和当前版本。若只有实现应有差异,请使用单独的工作树。
- 使用
-t <pattern>分别运行噪声较大的基准测试系列;一次运行所有测试的套件可能会干扰耗时极短的操作。 - 对于带分支的快速路径,应涵盖最佳情况、最差情况和符合预期的混合分布。
- 如果单次调用的耗时容易受到计时器/异常值噪声的影响,就在一次基准测试迭代中批量执行耗时极短的操作。
- 综合查看
hz、mean、p99/p999、rme和样本数。不要只相信一个有噪声的hz值。 - 如果
rme较高或 p999 存在较大的异常值,结果只能作为趋势参考;在作出判断前,应重新运行范围更窄的用例。 - 按照待测行为命名用例,让后续读者清楚哪些结果最重要。
- 在计时用例运行前验证正确性,避免基准测试测量无效或已失效的路径。
归因阶段
在将优化定为最终方案之前,证明生产代码中哪些确切改动应该保留:
- 保存未优化基线和完整候选版本的指标。
- 将生产代码差异拆分为逻辑独立的改动块或相互依赖的改动块组。如果仅语法或仅可读性的编辑可能影响生成的代码,也要纳入。
- 针对同一基线,单独对每个独立改动块进行基准测试。若改动块只有组合在一起才有效或会相互影响,则测试相关组合。
- 对每个改动块/改动块组,记录包指标;如果运行时开销可能变化,也要记录聚焦的性能测试结果。
- 只保留能改善包体积或性能的改动,或为正确性/测试/风格所必需且不会导致实测结果退化的改动。撤销仅为优化而做、效果持平或有害的改动。
- 重新构建并测量最终组合版本。除非保留的改动是正确性或风格所明确要求的,否则最终版本不得比归因前候选版本更大或更慢。
优化循环
- 使用
--name baseline测量基线场景。 - 检查差异、生成的 JS、各文件大小,以及必要时的分析来源。
- 先分析算法,再考虑语法。找出冗余循环、重复分支、重复扫描/切片/小写转换、分配开销较大的路径、搜索顺序和数据形态选择。
- 首先进行最小幅度且保持行为不变的算法改动,以减少工作量或代码形态;只有在穷尽算法优化方案后,才进行纯语法改动。
- 使用
--baseline baseline重新测量,只保留有明确证据支持的改进。 - 运行包的单元测试/类型检查、相关端到端测试,以及
git diff --check。 - 运行归因阶段,然后在最终确定前运行优化后的覆盖率/性能工作流。
算法优化
对于热点文件,将代码拆分为多个阶段,并根据移除的工作量优化每个阶段,而不是根据删掉的字符数:
- 解析/扫描:优先采用单次遍历,避免使用辅助扫描并分配子字符串;尽可能保留源字符串中的偏移量。
- 树构建:如果数据形状相同,就合并相同的节点创建分支;在局部变量中缓存重复使用的路由/选项字段。
- 匹配/搜索:保留优先级顺序,但只有在栈的推入顺序保持不变时才合并候选项循环;除非正确性需要,否则避免在后缀/前缀检查中分配内存。
- 提取/验证:按需计算参数,只在必要时保留状态;除非测试已覆盖,否则不要在跳过的分支或无路径分支之间复用部分参数。
- 排序/评分:只有在经过测量且仍然易读时,才替换辅助函数调用和比较器链。
- 排序/树后处理:如果完整遍历树只是为了对稀疏子数组排序,那么在构建期间,当数组长度达到 2 时记录这些待排序数组,然后一次性排序。
每次尝试优化后,都先运行有针对性的性能基准测试,再测量打包结果。若优化掩盖了运行时回退,或让不变量难以审查,就应舍弃。
优化后的覆盖率/性能工作流
优化完成后:
- 启动 5 个子 Agent,让他们根据现有测试审查优化差异。请每个子 Agent 找出可能因当前改动而失败的缺失单元测试用例或新暴露的边界情况,以及可能掩盖回退的缺失性能基准测试。
- 如果不确定是否存在回退,就询问用户或探索代码库,直到明确预期行为。
- 根据他们的意见添加有针对性的单元测试和基准测试。
- 仅提交测试、基准测试和支持测试脚本的改动。
- 暂存实现改动。
- 运行测试、性能基准测试和相关的包体积测量,然后将 BEFORE 结果写入
RESULT-optimization-{topic}.md。 - 弹出实现改动。
- 运行相同的测试、性能基准测试和包体积测量,然后将 AFTER 结果追加到同一文件。
- 查看基准测试输出时,考虑统计质量:标准差、误差范围、方差/噪声、样本数量和百分位数。若结果噪声较大,请重新运行或缩小结论范围。
- 比较 BEFORE 和 AFTER。如果出现任何回退,就继续迭代,直到结果正常,或撤销导致回退的改动。
实用模式:移除仅用于生产环境的字符串、移除未使用的导出、扁平化包装层、内联仅使用一次的辅助函数、避免重复字面量、改善 tree-shaking 边界,以及在保留行为的前提下简化分支。
DCE 与注解
Rolldown 只会移除未使用且无副作用的代码。属性读取可能触发 getter;存储/全局对象访问可能产生可观察行为或抛出异常。
| 注解 | 有效 | 不安全 |
|---|---|---|
/* @__PURE__ */ call() | 紧挨着调用/new 表达式之前,且未使用的结果可以被丢弃 | 声明、属性读取、初始化、存储、DOM/history/监听器代码 |
/* @__NO_SIDE_EFFECTS__ */ function f() | 函数的每次调用都没有副作用 | 访问全局对象、存储、DOM、history、订阅、警告或缓存的函数 |
sideEffects/模块标记 | 模块在未使用时没有导入时副作用 | CSS、polyfill、存储状态恢复、DOM/history 初始化 |
危险信号
- 使用软件包的
test:build作为大小代理指标。 - 相信源代码字节数或原始字节数,而不使用测量得出的
gzipBytes。 - 检查
dist/<metric-id>,而不是dist/<scenarioDir>。 - 仅仅因为字节目标较小,就给有副作用的代码添加 DCE 注解。
- 因为改动“只是影响包大小”,就跳过行为测试或基准测试。
- 跳过逐块归因,仅仅因为完整候选版本有所改进,就保留改动。
- 为了减少字节数而牺牲运行时性能、安全性、可读性或可维护性。
元工作
如果需要处理包大小测量或工作流本身,
请阅读 benchmarks/bundle-size/README.md