TanStack Router 仓库的包体积优化实战指南:以 `skills/bundle-size-optimization/SKILL.md` 为纲的测量、归因与迭代流程
发布时间:2026/9/16 16:32:16 作者:尧图编辑部 阅读量:1,286

TanStack Router 仓库的包体积优化实战指南以skills/bundle-size-optimization/SKILL.md为纲的测量、归因与迭代流程【免费下载链接】router A client-first, server-capable, fully type-safe router and full-stack framework for the web (React and more).项目地址: https://gitcode.com/GitHub_Trending/ro/router本指南基于 TanStack Router 仓库内 SKILL.mdskills/bundle-size-optimization/SKILL.md编写系统讲解如何在该仓库中优化真实测得的客户端 bundle而非源码文本从基准测量、命名快照、候选对比、源码归因source attribution到分块归因hunk-level attribution、算法级瘦身、死代码消除DCE注解安全边界以及收尾的覆盖/性能复检工作流。读完你将掌握一套测准 → 归因 → 改动 → 再测 → 复盘的闭环方法并能直接在benchmarks/bundle-size/场景与scripts/benchmarks/bundle-size/脚本上落地执行。一、先立真相源优化的是测量的客户端 bundle不是源码文本该 Skill 的第一条原则是Optimize measured client bundles, not source text即仓库内一切包体积优化的裁决依据是测量产物而非源码字节数。真相源有三个benchmarks/bundle-size:build—— 由 Nx 编排的基准构建任务benchmarks/bundle-size/results/current.json —— 当前测量结果汇总benchmarks/bundle-size/dist/下实际发射出的 JS 产物。本地迭代统一使用benchmark:bundle-size:run它与 CI 共用同一套测量脚本并通过 Nx 构建所选包。从 package.json 可以看到这些命令的注册关系benchmark:bundle-size→node scripts/benchmarks/bundle-size/build.mjsbenchmark:bundle-size:run→node scripts/benchmarks/bundle-size/run.mjsbenchmark:bundle-size:query/diff/history/analyze→ 各自对应的.mjs1.1 场景scenario模型仓库在 benchmarks/bundle-size/scenarios/ 下为每个包族准备了确定性 fixture。measure.mjs 中注册了全部场景核心划分如下包族场景说明tanstack/react-routerreact-router.minimal/react-router.full最小 vs 全量 hook/组件面tanstack/solid-routersolid-router.minimal/solid-router.full同上Solid 版本tanstack/vue-routervue-router.minimal/vue-router.full同上Vue 版本tanstack/react-startreact-start.minimal/react-start.full/react-start.deferred-hydration/react-start.query-integration额外覆盖createServerFn、createMiddleware、useServerFn与Hydratetanstack/solid-startsolid-start.minimal/solid-start.full/solid-start.deferred-hydration同上tanstack/react-startRsbuildreact-start.rsbuild.minimal/react-start.rsbuild.minimal-iife/react-start.rsbuild.full使用tanstack/react-start/plugin/rsbuild的工具链变体其中minimal场景是一个只有__root 首页、渲染hello world的小型路由应用full场景在相同路由形状上通过根级 harness 引入/使用完整 hooks 与组件面。以 react-router-minimal/src/routes/__root.tsx 与 index.tsx 为例可以看到最小场景的真实形态其 vite.config.ts 使用tanstack/router-plugin/vite并开启autoCodeSplitting: true这正是仓库推荐的默认文件路由风格。场景 ID 与产物目录的对应关系需要特别注意dist 路径使用scenarioDir/outDir而不是 metric id。例如react-router.minimal映射到benchmarks/bundle-size/dist/react-router-minimal/而react-start.rsbuild.minimal则输出到react-start-rsbuild-minimal/。检查产物时若直接看dist/metric-id属于 Red Flags 之一。1.2 测量指标测量脚本对所有发射出的客户端 JS chunk 计算三种字节数见 measure.mjs 的sizesForFilesrawBytes原始字节、gzipBytesgzip 压缩后、brotliBytesBrotli 压缩后并记录每个文件的独立尺寸到files数组。此外还通过 Vite manifest 递归收集入口图内的 JS 文件collectInitialJsFiles生成initialRawBytes/initialGzipBytes/initialBrotliBytes作为初始加载图上下文。追踪信号优先级首先看gzipBytesPR 增量与历史图表的首要信号其次看initialGzipBytes、rawBytes、brotliBytes、jsFiles总数与逐文件files。一个反复出现的坑是gzip 可能和 raw 反向变动——即微小改动后 raw 字节上升但 gzip 反而下降或反之因此每次候选改动后都应重新测量不能凭 raw 字节直觉判断。二、命令速查从全量基准到符号引用Skill 的命令表覆盖了优化迭代的完整生命周期下面是逐条展开需求命令全量基准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 last2.1 命名运行与快照语义从 run.mjs 的实现可以看到命名运行--name name把指标与日志写入benchmarks/bundle-size/results/runs/name/且已有命名结果不可覆盖namedRunDir会先检查current.json是否存在。命名必须以字母或数字开头且只允许字母、数字、连字符、下划线。不带--name时运行会覆盖results/current.json及本次执行步骤对应的日志。--baseline接受一个运行名或 JSON 文件路径按/、\或.json后缀判定--results-dir dir可整体更换结果根目录。发射产物默认仍写入共享dist/如需保留候选产物需传--dist-dir dir单独指定。对命名结果执行 query / diff / analyze 时需显式传--current benchmarks/bundle-size/results/runs/name/current.json。基线校验baseline 必须包含非空metrics数组且基线不能是当前输出本身会报错提示先用--name保存。2.2 Runner 行为细节Runnerrun.mjs通过step()依次执行测试可选→ 测量 → 报告并强制设置CI1、NX_DAEMONfalse、FORCE_COLOR0三个环境变量完整 stdout/stderr 落入tests.log与measure.log紧挨着current.json。失败时 runner 停止并以非零码退出只打印最后 40 行、截断到 8000 字符的日志尾部——因此quiet stdout 不代表 Nx 挂起应先检查报告的日志再决定是否走 Nx 重置/重试护栏。注意bundle-size 的正向增量变大不会导致命令失败。2.3 测试与测量的串联--test-projects先通过 Nxrun-many --targettest:unit串行运行所选包的单测--之后的所有参数只透传给这些测试。测试与测量顺序执行测试失败会在测量前中止从而避免旧结果产生误导性 diff。Skill 强调 runner 不会自动选择测试——类型测试、性能基准.bench.ts与 e2e 测试必须保持独立命令。三、Rules一套可执行的约束清单Skill 的Rules部分实际上是长期实践沉淀的操作纪律逐条展开如下一次只跑一个 Nx 命令环境变量、重定向、退出码、日志尾部与结果查询都交给 runner而不是 shell 链式拼接。Agent 运行时用pnpm --silent benchmark:bundle-size:run ...去掉包管理器的命令回显。--test-projects只填发生改动的包不要把类型测试、性能基准、e2e 混进同一批。基线与候选的场景选择、测量 flags 必须完全一致每个需要保留的快照都用新的--name。包源码改动后不要加--skip-package-builds默认流程会通过 Nx 重建并保留有效包缓存手动跳过反而可能用上过期产物。先盯gzipBytes再依次看initialGzipBytes、rawBytes、brotliBytes、jsFiles和逐文件files。dist 路径用scenarioDir/outDir不是 metric idreact-router.minimal→dist/react-router-minimal/。微小改动要每个候选后都测量gzip 可能相对 raw 反向变动。对比基准 commit 时在独立 worktree 中测量同一场景把它保存的current.json路径传给--baseline。历史数据用于观察既有模式与基线不用作源码归因——它是 commit 级数据。运行期性能与安全永远不能为包体积让步。此外还有两条关于继续迭代与可读性的硬规则不要拿到第一个验证通过的结果就停手应在合理范围内持续尝试本地、发射 JS 与算法级候选直到出现测量回退、可读性劣化或风险超限内联 helper 或简化非显然逻辑时用简短注释说明含义/不变量而非机械步骤保持可读性。3.1 删除 helper 前的引用检查内联或删除任何 helper/函数前必须先用 TypeScript language-service 脚本确认引用pnpm ts:symbol-references -- --project package/tsconfig.json --file decl-file --symbol name该命令由 ts:symbol-references 注册底层是 scripts/ts-symbol-references.mjs。若 helper 在其他地方还有引用仅为包体积而内联单处调用通常不值得除非测量证明有效若已无引用则删除并用脚本复核。包改动后要跑该包的单测/类型测试以及 e2e/ 下相关 e2e 测试。四、Benchmark Rules性能基准与包体积测量如何协同Bundle 体积与运行期性能在优化中经常互相拉扯Skill 为此制定了并行规则迭代期选一个最可能包含改动代码的场景router-core/react-router 默认用react-router.minimalsolid-router 用solid-router.minimalvue-router 用vue-router.minimalReact Start 用react-start.minimal或react-start.rsbuild.minimalSolid Start 用solid-start.minimal。若代码只被更全的场景拉入例如某个 hook 只被solid-router.full引用就覆盖默认改用solid-router.full迭代。改动跨多个包族时先选能 import 共享代码的最小场景快速迭代定稿前再抽查下一个最可能受影响的包族。定稿前必须跑不带--scenario的全量基准并对比所有场景即使目标场景已改善也要排查离群/异常。直接基准被改动的机制本身而非其外围公共 API。用宽泛的真实场景做冒烟/回归覆盖用聚焦用例做证明。基线与当前必须用同一份基准文件仅实现不同时用独立 worktree。统计质量方面只信一个 noisy 的hz不行要综合读hz、mean、p99/p999、rme与样本数高rme或大的 p999 离群值只能当方向性信号重跑更窄的用例后再下结论。分支密集的快路径要覆盖最好、最坏与预期混合分布超快操作要批量放进同一迭代避免被计时器/离群噪声主导用例命名应反映被测行为且先验证正确性再计时避免测到无效或死路径。五、Attribution Round逐 hunk 归因证明哪些改动真正该留调用一个优化定稿之前Skill 要求做一次严格的归因轮次防止整包变好了就全留下的粗放决策快照未优化的基线与完整候选的全部指标。把生产 diff 拆成逻辑 hunk 或相互依赖的 hunk 组语法级、可读性级改动若可能影响发射代码也一并算入。让每个独立 hunk单独对同一基线跑基准只在组合才生效或互相交互的 hunk 要测相关组合。每个 hunk/组记录 bundle 指标运行期成本可能变化时还要记录聚焦的性能结果。只保留真正改善体积/性能、或为正确性/测试/风格所必需且不拖累测量结果的改动中性或有害的纯优化改动一律回退。重建并复测最终组合版本——它不得大于或慢于归因前的候选除非保留的改动明确为正确性或风格所必需。六、Optimization Loop 与 Algorithmic Pass先算法后语法推荐的迭代循环是用--name baseline测量基线场景检查 diff、发射 JS、逐文件大小必要时看分析attribution来源先分析算法再动语法——识别冗余循环、重复分支、重复扫描/切片/lowercase、高分配路径、搜索顺序与数据结构选择先做最小且行为保持的算法级改动移除无效工作或代码形态语法级改动留到算法候选穷尽之后用--baseline baseline复测只保留验证有效的胜利跑包的单测/类型、相关 e2e 与git diff --check做归因轮次再走收尾的覆盖/性能工作流。6.1 按阶段拆分热文件对热文件按阶段逐段优化以移除的工作量而非移除的字符数为准解析/扫描优先单遍扫描避免helper 扫描 子串分配的组合尽量保留对源字符串的 offset。树/构建数据形状共享时合并相同的节点创建分支把重复读取的 route/options 字段缓存到局部变量。匹配/搜索保持优先级顺序只有栈压入顺序完全一致时才合并候选循环后缀/前缀检查除非正确性需要否则避免分配。提取/校验惰性计算 params只在需要处携带状态除非有测试覆盖否则不要跨被跳过的/无 path 的分支复用部分 params。排序/打分替换 helper 调用与比较器阶梯要经过测量且保持可读。排序/树后处理若整树遍历只为排序稀疏子数组可在构建期记录达到可排序状态长度到 2的数组再一次性排序已记录的数组。每个候选之后先跑聚焦性能基准再做 bundle 测量拒绝隐藏运行期回退或使不变量难以审计的胜利。七、Post-Optimization Coverage/Perf Workflow收尾复检优化完成后按以下步骤收尾派 5 个子代理审查优化 diff 与既有测试的关系各自找出可能被当前改动击穿的缺失单测用例、新暴露的边界情况以及可能隐藏回退的缺失性能基准回退行为不明确时先问用户或继续探索代码库直到行为预期清晰依据反馈补充聚焦的单测与基准只提交测试/基准/测试脚本类改动stash 实现改动跑测试、性能基准与相关 bundle 测量把 BEFORE 结果写入RESULT-optimization-{topic}.mdpop 实现改动重跑同样的测试、性能基准与 bundle 测量把 AFTER 结果追加到同一文件评估基准输出时看统计质量标准差、误差幅度、方差/噪声、样本数、百分位数噪声大就重跑或收窄结论对比 BEFORE/AFTER任何一项回退都迭代到绿或回退该改动。Skill 同时给出了一批可复用的模式清单移除仅生产环境使用的字符串、移除未使用的导出、扁平化包装层、内联单次使用的 helper、避免重复字面量、改善 treeshaking 边界、在保持行为后简化分支。八、DCE And AnnotationsRolldown 树摇与注解安全边界Rolldown只在代码未被使用且无副作用时才移除代码。属性读取可能触发 getterstorage/全局访问可能被观测或抛错——因此看起来没用的代码未必安全可删。三类注解各有明确的安全与不安全边界注解有效不安全/* __PURE__ */ call()紧邻某个调用/new 表达式之前且其未使用结果可被丢弃声明、属性读取、setup、storage、DOM/history/listener 代码/* __NO_SIDE_EFFECTS__ */ function f()该函数每一次调用都无副作用触碰 globals、storage、DOM、history、subscriptions、warnings、caches 的函数sideEffects/module flags模块未被使用时没有 import 期副作用CSS、polyfills、storage hydration、DOM/history setup核心纪律是绝不为凑字节目标给有副作用的代码加 DCE 注解——这是 Skill 明确列出的 Red Flag。九、Red Flags 自查清单收尾前对照以下红线逐条自检❌ 用包的test:build当作体积代理❌ 信任 source 字节或 raw 字节而不是实测的gzipBytes❌ 检查dist/metric-id而不是dist/scenarioDir❌ 因为字节目标小就给有效果的代码加 DCE 注解❌ 因为改动只是包体积就跳过行为或基准测试❌ 跳过逐 hunk 归因只因为完整候选整体改善了就保留改动❌ 用运行期性能、安全、可读性或可维护性去换字节。十、延伸阅读仓库内的配套资源基准场景定义benchmarks/bundle-size/scenarios/含各包族 minimal/full 与 Start deferred-hydration 变体测量与报告脚本scripts/benchmarks/bundle-size/measure.mjs、run.mjs、report.mjs、diff.mjs、history.mjs、query.mjs、analyze.mjs基准说明文档benchmarks/bundle-size/README.md含 CI 报告、GitHub Pages 历史图表与 backfill 接口说明命令注册package.json符号引用检查脚本scripts/ts-symbol-references.mjs性能基准示例Skill 提到的聚焦*.bench.ts模式packages/router-core/tests/ 下的 bench 文件如closing-tag-detection.bench.ts说明本文所有命令与配置均以当前仓库实际内容为准适用于在本仓库根目录运行 pnpm 脚本的场景测量是确定性的本地流程任何结论都应基于实测的gzipBytes与归因结果而非直觉或源码字节数。【免费下载链接】router A client-first, server-capable, fully type-safe router and full-stack framework for the web (React and more).项目地址: https://gitcode.com/GitHub_Trending/ro/router创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考