关于 rspeedy-bundle-size
用于分析并缩减 rspeedy/Lynx 应用交付包体积的测量优先技能。它帮助开发者定位主要体积来源、追踪真实模块关系,并依据构建证据选择优化方向。
主要功能
- 01优化前测量包体组成
- 02区分媒体与 JavaScript 优化方向
- 03从构建统计追踪导入边
- 04构建前筛查多个项目
- 05按仓库情况选择构建命令
适用场景
- 01排查 rspeedy 包体为何过大
- 02构建后确定包体优化优先级
- 03追踪哪些模块将代码引入入口
- 04筛选多个 Lynx 应用以便深入测量
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ lynx-community/ skills --skill rspeedy-bundle-size安装后在 Claude Code 中输入/rspeedy-bundle-size确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/cross-project-triage.mddual-thread-architecture.mdmeasure-with-rsdoctor.mdrspeedy-gotchas.mdthree-levers.mdreferences/assets/gate.awkmt-cutpoint-analyzer.mjsmt-leak-analyzer.mjsplugin-strip-mt-telemetry.tsscan-levers.mjssignatures.cjsstrip-mt-telemetry-loader.cjs详细介绍
Rspeedy 包优化
采用先测量再优化的工作流,缩小 rspeedy/Lynx 应用的包体积。核心原则:不要猜测字节都花在哪里——先测量,再优先解决影响最大的因素。 许多“优化包体积”的需求之所以失败,是因为一上来就做细枝末节的树摇优化,而真正占空间的却是媒体资源或主线程泄漏。
对照源码验证——不要猜测
rspeedy / ReactLynx 的构建行为并不直观,靠猜只会让你自信地犯错。如果不确定构建、各层、ES 目标或 stats/rsdoctor 输出的实际行为,请阅读源码——不要根据名称或记忆推断。 整套工具链都是开源的;可以克隆或浏览代码,并使用 grep 搜索相关插件或工具函数:
工具链分层为 Rspack → Rsbuild → Rspeedy(每一层都基于前一层构建),因此某项行为可能存在于任意一层——沿着依赖关系逐层查找:
- rspeedy 和 ReactLynx 插件(
lynx-stack单体仓库):https://github.com/lynx-family/lynx-stack - rsbuild(rspeedy 构建所基于的 Rsbuild 层):https://github.com/web-infra-dev/rsbuild
- rspack(底层打包工具;stats/模块类型、layers、
output.environment):https://github.com/web-infra-dev/rspack - rsdoctor:https://github.com/web-infra-dev/rsdoctor
(这里不固定具体文件路径——路径会随版本变化。请按符号搜索,例如 ES 目标工具函数、stats-json 插件、入口/layer 配置。)本指南中的几项事实——main=ES2019 / background=ES2015、stats.json 包含 layer、入口拆分为两层——均通过阅读源码或运行真实构建进行了确认,而非凭空假设。新的说法也应遵循同样的标准。
NO GUESS — 追踪真实模块图,绝不臆测导入链
当问题是 “为什么 X 被打包 / 谁导入了 Y / 从入口到 Z 的链路是什么” 时,你必须 追踪真实模块图——绝不能根据 package.json、直觉或半截答案(例如 “它是通过依赖树引入的,被作用域提升进了 ./entry.tsx + 4630 modules。”)描述一条看似合理的链路。这个 + N modules 是打包器在隐藏导入边,不是答案。要找到真实的边:
- 使用
tools.rspack构建时,将optimization.moduleIds: 'named'和optimization.concatenateModules: false一并设置。作用域提升(concatenateModules: true,生产环境默认值)会 折叠导入边——每个被提升的模块,其导入方都会显示为整个拼接组("./entry.tsx + N modules"),因此你根本看不到谁导入了什么。追踪链路时必须禁用它(这会显著增大体积——一个项目实测约增加 70%,具体取决于模块图——所以它只用于分析;完成后要恢复)。取消拼接还会禁用__split__,因此已经从主线程拆分出去的组件会重新膨胀到main-thread.js中——将“巨大的弹窗/模态框”判定为延迟加载候选项前,请与真实构建结果交叉验证(见 rspeedy-gotchas.md §11)。 - 从
stats.json的reasons[].moduleName中读取 真实导入边(每个模块都会列出导入它的模块)。大型多入口构建的这个文件可能达到 数百 MB 到约 1 GB——过大,无法使用readFileSync(会出现ERR_STRING_TOO_LONG);请使用流式读取(grep/readline)。(使用默认 stats 预设的单页构建要小得多——只有stats: { all: true }或合并许多页面时,文件才会膨胀;见 measure-with-rsdoctor.md。) - 一跳一跳地追踪链路,并 报告每一跳的确切文件。如果无法用真实的 reason 边证实某一跳,你就没有这条链路——应如实说明,不要编造。
这条规则源自一个真实案例:某个应用膨胀出的 1388 个图标 看起来像是“应用 → 业务组件库 → UI 库 → 图标总入口”,但应用 没有直接导入这些包(在 src/ 中查找结果为 0);只有取消拼接后的 reasons 才揭示了真实边。描述一条臆测的链路会让维护者去查错文件。没有真实 reason 边作为依据的依赖链就是猜测;不要把猜测说成链路。
何时使用此技能
- “帮我优化一下包体积” / “减少这个 rspeedy 应用的包体积”
- “为什么我的包这么大” / “是什么导致这个包体积这么大”
- 构建后,用户希望拆解体积并获得按优先级排序的计划
- 判断是否值得进行 tree-shaking,而不是压缩媒体或清理主线程
导致包体积过大的另一个独立原因是 mangle 失败——生产代码块中保留了可读的、带模块前缀的名称(common_EVENT_NAME、utils_toSomething),通常是 ModuleConcatenation 下的 eval() 污染所致。相关内容作为 JS 优化手段的一部分,见 references/three-levers.md §Lever 2。
何时不要使用此技能
这 仅针对包体积 / .lynx.bundle——优化的是 交付的字节数,而非其他方面。以下情况请改用其他技能:
- 运行时 / 渲染性能(卡顿、首帧缓慢、掉帧)——这是另一个问题;此技能不会影响执行速度。
- JS 内存 / 堆泄漏——使用
lynx-js-heap-snapshot-analysis。 - 指令的 语义(
'background only'的有效形式 / 行为)——请使用reactlynx-best-practices;此技能只将该指令 作为体积优化手段应用。 - 深入分析
rsdoctor-data.json(每模块保留集、重复依赖树)——使用rsdoctor-analysis;此技能只做概要分析。 - TS 类型 / tsconfig →
lynx-typescript。解码 tasm 二进制格式 →lynx-tasm-codec。
相关技能
如果你可以访问这些技能,它们会深入讲解相关问题:
rsdoctor-analysis— 深入分析rsdoctor-data.json(tree-shaking 保留的模块、重复包、副作用)。当需要进行本技能仅作概述的深度 rsdoctor 查询时,请使用它。公开地址:https://github.com/rstackjs/agent-skills/tree/main/skills/rsdoctor-analysisreactlynx-best-practices— 双线程指令的权威参考,包括'background only'/'background-only'函数指令(参见其detect-background-only规则)。请参考它了解指令语义和可接受形式,不要在此重新推导。rushstack-best-practices— 用于 Rush 单体仓库(检测rush.json/common/config/rush)。Rush 仓库通常应使用rush、rushx或rush-pnpm,而不是直接使用pnpm。公开地址:https://github.com/microsoft/rushstack/blob/main/skills/rushstack-best-practices/SKILL.md
构建系统说明
开始测量前,先确定仓库使用的构建编排工具,并使用其原生命令入口,不要想当然地使用 pnpm build:
- Rush 单体仓库 — 如果存在
rush.json或common/config/rush/,请使用 Rush 技能。从项目目录运行项目脚本时使用rushx <script>,或使用范围限定的仓库命令,例如rush build --to .;只有确实需要 pnpm 命令时才使用rush-pnpm。 - 自定义包装工具 — 如果脚本或配置通过公司/单体仓库 CLI 封装了
rspeedy,请优先使用文档中说明的入口,而不是直接调用rspeedy/pnpm。 - 普通 pnpm/npm 项目 — 如果仓库不由 Rush 管理且没有包装工具,则直接使用
pnpm/npm。
不要盲目运行 pnpm build。先阅读 package.json、根目录工作区文件和现有项目文档,然后选择范围最小、能够构建目标 rspeedy 应用的命令,同时保留仓库的环境设置。如果项目固定了 Node 版本,请通过 Node 版本管理器使用该版本(例如 fnm exec --using=<version> -- ... 或 nvm exec <version> ...)。
项目很多时,先做分流(别搭建整支舰队)
下面按项目介绍的工作流是先测量再行动——但如果你接手的是一支项目舰队(例如“优化我们的 Lynx 包”,或一个有 50 多个应用的仪表板),就不能把它们全都构建一遍再决定从哪里着手。先做一次成本低廉的源码级分流,排出值得实际构建的项目和优化杠杆,再对胜出项目采用先测量再行动的方式。分流命中表示“在这里测量”,并不代表“已确认存在冗余”。
扫描器 references/assets/scan-levers.mjs 会遍历各项目的源码(不进行构建),并按未设保护的后台专用泄漏、缺少 sideEffects、内联或大型媒体资源,以及重复依赖进行排序:
node references/assets/scan-levers.mjs --glob <reposRoot> # rank a fleet
node references/assets/scan-levers.mjs <projectDir> --json out.json
阅读 references/cross-project-triage.md,了解如何解读结果及其陷阱(重复依赖计数主要受开发工具链影响,大多是噪声;源码中存在 ≠ 主线程中存在;遥测特征会因某个属性名称而高估)。根据对一支真实的约 370 个项目的舰队所做的实测:约 74% 的项目在源码中存在后台专用泄漏信号,约 62% 的项目大多未设保护,约 99% 的项目缺少 sideEffects——遥测和 jsb/原生桥接代码几乎在所有项目中都有引用,而且几乎总是未设保护。
遥测/日志记录只有在由 jsb 支持时才是后台专用的——由 console.* 支持的日志记录器在主线程上也会运行,因此,在将遥测调用视为可剥离内容之前,请先确认底层实现。如果确实由 jsb 支持,它就是一个广泛相关的 Lever-3 信号——但采取行动前仍需测量。盲目剥离(plugin-strip-mt-telemetry.ts + loader,方法列表可自定义)在一个真实项目中仅净减 −3.9 kB(−0.08%),因为日志记录器的导入仍然保留了下来(遵循最后引用规则),所以应将其视为测量辅助工具,而非解决方案(参见 three-levers.md §Lever 3)——它不是一种“加进去就能见效”的优化杠杆。相比之下,只需改一行配置 extractStr: true,就在一轮包含 6 个项目的舰队测试中实现了 −1.88% 至 −5.99% 的优化(在一个有 64 个页面、字符串密集型的应用中最多减少 −1.29 MB;精简演示应用或重复字符串内容较少的应用中则几乎没有变化)。因此,对于尚未使用 extractStr 的字符串密集型应用,直接启用它就是投资回报率最高的第一步——效果远胜于手动追查遥测泄漏。(只需设置 extractStr: true;通常不建议调整 strLength 阈值。)
心智模型:三层,三种杠杆
Lynx 应用包由两个线程和非 JS 资源组成。体积分布在三个部分,且每个部分的优化方法都不同——把它们混为一谈是最常见的错误。
| 杠杆 | 内容 | 典型体积 | 负责人 |
|---|---|---|---|
| 媒体资源 | 内联或随包发布的图片/字体 | 通常是最大的一块(数 MB) | 应用代码——通常收益最高 |
| JS — 后台线程 | react:background 层、应用逻辑 | 各不相同 | 应用代码——摇树优化、去重 |
| JS — 主线程 | react:main-thread 层、首屏渲染路径 | 应该很小;可能出现泄漏 | 跟踪分析 → 模块拆分(标记 ≈ 0);较大的依赖链需要向库方提出需求 |
请参阅 references/three-levers.md,了解如何衡量并处理每一层。
如果你还不知道为什么有两个 JS 层,请先阅读 references/dual-thread-architecture.md,再进行优化——其中包含完整模型,以及每项杠杆 3 修复所依赖的 代码裁剪 宏表。简而言之:主线程负责首屏直接渲染,必须保持精简;后台线程承载逻辑、生命周期和状态。构建会生成 main-thread.js 和 background.js,并编译为引擎发布的二进制文件 .lynx.bundle。ReactLynx 会自动将 useEffect/componentDidMount/bindtap 回调从 main-thread.js 中摇掉;如果无法判断,就用 'background only' 或 __BACKGROUND__/__MAIN_THREAD__ 宏引导它——这个机制就是主线程泄漏优化杠杆。
核心工作流
第 1 步 — 衡量(绝不跳过)
在提出任何建议前,先获取真实的模块级明细。即使用户已经提供了粗略体积(例如“图片约 10MB,JS 约 4MB”),或要求你只做某项具体改动,也必须如此——查看输出体积只能作为初步假设,不能代替模块级衡量。务必通过 rsdoctor(或 stats.json)确认明细,并先给出按优先级排序的建议,再决定是否采用或实施任何优化杠杆,包括用户明确要求的改动。如果用户要求的改动选错了杠杆(收益低),应先说明;只有衡量结果确认值得做之后,才继续实施。
rspeedy 的官方方式是通过配置启用 rsdoctor——不要手动安装 @rsdoctor/rspack-plugin,也不要反向分析内部 fork。在 lynx.config.ts 中添加:
tools: {
rsdoctor: {
disableClientServer: true,
brief: { writeDataJson: true },
},
}
然后使用 RSDOCTOR=true 构建。完整详情、允许的配置字段,以及分片数据重建技巧,请参阅 references/measure-with-rsdoctor.md。
使用上一节中与构建系统对应的命令:
# Rush project script
RSDOCTOR=true rushx build
# Wrapper-tooling project (example; prefer the repo's documented command)
RSDOCTOR=true <wrapper> build
# Plain package script
RSDOCTOR=true pnpm build
快速初步查看时,也可以使用更轻量的替代方案:rspeedy 内置的 stats.json——在配置中启用 performance.profile: true,或直接使用 DEBUG=rspeedy 构建(会写入 output/stats.json)。它确实为每个模块提供 layer 字段(react:main-thread / react:background,已在示例项目中验证),因此可以区分两个线程。如果需要模块级 gzip 体积,以及免费的重复项/摇树优化分析,优先使用 rsdoctor;若只想快速按层划分体积且不做额外配置,则使用 stats.json。详情请参阅 references/measure-with-rsdoctor.md。
第 2 步 — 将体积分到三层
根据 rsdoctor 的 moduleGraph,按以下类别汇总 module.size.parsedSize(以及 gzipSize):
- 媒体 / 资源 — 非 JS 模块或资源模块
- JS
react:background—module.layer包含 background - JS
react:main-thread—module.layer包含 main-thread
先报告这三项总量。仅凭这些数字,通常就能决定整个优化方向——例如:“你的 14MB 中有 10MB 是图片;JS 摇树优化大约只能节省 50KB,所以应从媒体资源着手。”
第 3 步 — 根据 ROI 选择杠杆,按顺序着手
默认优先级(如果数据表明应采用其他顺序,则以数据为准):
1. Media assets → biggest, easiest wins (compress, dedup, format)
2. JS background → tree-shaking, duplicate packages, deep imports
3. Main-thread leak → background-only code that leaked into render path
4. Compile-layer knobs → extractStr, CSS minify, debug-info, lazy bundle — often beats the JS tail
各项杠杆的具体技巧和决策要点,请参阅 references/three-levers.md(杠杆 1–4)。
第 4 步 — 知道何时该停
经过充分优化的项目会遇到瓶颈:摇树优化实际上已无可压缩(sideEffects 设置正确——或者,考虑到约 99% 的项目都省略了该标记,压缩器已经执行了等效的 DCE),代码级 DCE 在 bundle 层面几乎没有收益,而剩余的主线程开销来自你无法修改的 SDK 库。**要直截了当地说明这一点。**不要为了显得忙碌而硬凑边际改动——应报告瓶颈,并说明哪些因素可以突破它(例如库侧的 'background only' 指令、CI 中的资源管线改动等)。
结构性重复的瓶颈(大型页面中最常见的情况——通过深入分析两个真实的大型应用验证)。大型 .lynx.bundle 通常并非由开销或死代码主导,而是由首屏直接渲染确实需要的庞大资源主导,且该资源同时存在于两层中(在双线程模型下,主线程和后台线程各自打包一份)。两个真实案例:一个应用的页面中,87% 是 i18n locale JSON(全部 57 种语言,在两层中都有);另一个应用有 62% 是类似 Pixi 的渲染引擎(每层约 1.4 MB),用于绘制首屏。遇到这种情况时,常见的应用侧杠杆从根本上都无效,说明原因就是交付成果:
- 无法通过懒加载拆分——这是首屏直接渲染内容;延后加载会让首屏在渲染过程中加载代码块(导致更慢/出现 FOUC)。(这是懒加载 bundle 规则的应用:首屏需要的内容绝不懒加载。)
- 剥离/DCE/去重都不适用——它既不是死代码,也不是重复的包;而是同一份必要资源合理地存在于两个线程中。
extractStr只对字符串类型的情况有帮助——它会去重两层中重复的字符串字面量(i18n locale 文本、类名——这就是字符串占比高的应用使用它后体积会大幅缩减的原因),但不会去重重复的代码(例如渲染引擎),因此无法解决引擎的情况。
因此,真正的杠杆在于库/框架层面,而非应用侧:使用更轻量的库、通过框架功能在两个线程间共享代码,或由宿主同步注入资源(例如当前 locale),从而无需将其打包进去。测量占主导地位的代码块,指出其名称,并说明需要上述哪种方案——不要在页面体积由结构决定时,仍埋头调整应用侧配置。
rspeedy/Lynx 特有的注意事项
如果把这当作一个通用 webpack 项目来处理,有好几件事会让你踩坑。修改配置或断言发现之前,请阅读 references/rspeedy-gotchas.md。主要陷阱如下:
- 两个线程使用固定的 ES 目标版本——主线程为 ES2019,后台线程为 ES2015(生产环境);两者都会将
let/const降级为var,以提升解析速度。这些是由引擎决定的平台常量;不要提议通过调整目标版本来减小字节数。(部分项目会将output.environment固定为更低的版本,甚至完整的 ES5,以缩短解析时间——这是有意为之的权衡,并非常态。) - rsdoctor 数据可能分片存储(位于
.rsdoctor/目录,而不是单个 JSON 文件)——必须正确重建数据,否则数字就会出错。 - 构建需要使用固定版本的 Node(通过 fnm/nvm,例如
fnm exec --using=20.19.0或nvm exec 20.19.0)——较新的 Node 可能会导致 rspeedy 配置加载失败。
日志规范——每项优化都要在 .bundle-opt-log/ 中记一条
**任何优化(分析或实施)都必须:**在被优化项目的根目录下创建隐藏的 .bundle-opt-log/ 目录,并在其中记录一条日志(例如 apps/<app>/.bundle-opt-log/)。日志的主要指标是每个页面的 .lynx.bundle 大小变化前 → 后(即交付的二进制文件)。
每条记录(每轮一个 Markdown 文件,以日期 + 优化手段命名,例如 2026-06-04-icon-treeshake.md)包含:
- 项目 / 页面,以及
.lynx.bundle的优化前和优化后大小(kB),并注明 Δ 和百分比 - 实施的优化手段(一行),以及状态是已实施 / 已提议 / 遇到瓶颈
- 验证构建命令(以便复现数据),以及固定时使用的 Node 版本
- 如有测量注意事项(例如热缓存与冷缓存的差异——参见注意事项 §7;低于约 3% 的变化需要在热缓存下重新测量)
还要维护一份整合的跟踪表(例如 .bundle-opt-log/优化记录表.md / SUMMARY.md):一张 Markdown 表,每项优化占一行——优化项 | .lynx.bundle 前 | 后 | Δ | 机制 | commit——另加一行累计总计;再附一张表,记录已调查但未发布的优化手段(包含测得的 Δ,通常约为 0,以及原因)。每次完成一项优化后都追加一行;前后数据要衔接,使每行的“前” = 上一行的“后”。这是一份便于快速查看的台账;每轮文件中则记录详细信息。负责人特别要求提供这张表——请保持更新。
以 .lynx.bundle 为核心:中间产物 main-thread.js / background.js 的数据仅作辅助说明,重点始终是交付的 .lynx.bundle 优化前后的数据。将构建产物(中间文件、分析用反拼接转储文件)放在被 git 忽略的构建输出目录中(output/ 或 dist/),不要放进新的顶层目录——仓库中只应保留 .bundle-opt-log/ 下的 Markdown 文件。创建该目录前,请与负责人确认 .bundle-opt-log/ 应提交到 git 还是加入 git 忽略列表。
原则:先报告,默认只读
与此处的其他分析技能一样,默认应当生成有优先级的报告,而非编辑代码。展示发现、选项和权衡;由用户决定要做哪些更改。只有在用户明确要求时才修改应用代码,并确保改动精准且范围有限。
响应格式
- 测量明细——三层的总量(媒体 / bg JS / main JS)及各层最大的模块
- 最大优化手段——哪一层占主导,以及原因
- 优先操作项——操作 → 预估节省量 → 工作量 → 负责人(应用 / 库 / CI)的表格
- 主线程泄漏追踪——如有泄漏,列出可疑模块及其上游导入路径 / 负责人;不要只报告泄漏的 API 字符串
- 编译层快速优化项——尝试过或提议的
extractStr/ CSS 压缩 / 调试信息 / lazy-bundle 开关(优化手段 4) - 瓶颈——哪些部分已经达到最优,以及要进一步优化需要哪些外部变更
- 日志记录——写入
.bundle-opt-log/条目并更新跟踪表(参见日志规范)