- 前端
- UI组件
【免费下载链接】motion
A modern animation library for React and JavaScript
本文以 plans 目录下的执行计划 plans/035-bundle-size-budget-enforcement.md 为骨架,结合仓库中实际的预算检查脚本、package.json 脚本与 CircleCI 配置源码,完整呈现 motion(framer-motion + motion-dom)如何把"包体积预算"从一条无人执行的规则,升级为 CI 与发布流程中不可绕过的阻塞闸门,并重新校准基线。读完本文,你将掌握:体积检查脚本的工作原理、预算如何被定义与度量、prepack 与 CI 两条阻塞链路如何接线,以及"预算棘轮(ratchet)"这一防回归机制的运作方式。
对 motion 这样的动画库来说,交付的字节数本身就是头号卖点。仓库早已具备相当成熟的体积工具链:按入口粒度打包的rollup.size.config.mjs、每个包package.json#bundlesize中声明的 gzip 预算,以及由dev/inc/bundlesize.mjs执行的预算核对脚本。然而在 Plan 035 诞生之前,没有任何东西真正运行过这个检查:CircleCI 上不存在 measure 任务,发布路径(prepare/prepack)也只跑 rollup 的measure任务而不会调用预算脚本。结果是,在计划基线提交42bfbe3ed上实测,9 条预算中有 6 条处于失败状态,主入口<motion.div>包从 v12.23.24(2025 年 10 月)的 35.65 kB gz 漂移到了 38.56 kB gz(+10%),整个过程没有任何信号暴露问题。预算上一次校准还停留在 2025 年 4 月(提交596e0eee8)。Plan 035 正是为此而设:把预算重新校准到现实基线,并把既有的检查脚本接入 CI 与发布流程,让未来的体积增长变成"刻意决策"而非"无声漂移"。
现状盘点:工具齐全,唯独无人执行
Plan 035 首先对仓库的"体积治理现状"做了一次精确盘点,逐条列出相关文件与脚本的当前行为,这些都可以在仓库源码中逐一印证:
dev/inc/bundlesize.mjs—— 预算检查器本体。它读取每个包的package.json#bundlesize数组,对其中列出的每个 dist 文件做 gzip(zlib 默认压缩级别),任何一条超标即以退出码 1 终止。关键限制在于:它基于process.cwd()解析路径(见 bundlesize.mjs 的packagePath拼接,以及 bundlesize.mjs 的fullPath拼接),因此今天它只可能在仓库根目录下运行。它还接受一个可选的包名参数(framer-motion或motion-dom)。- 根
package.json:package.json 的"measure": "turbo run measure --force && node dev/inc/bundlesize.mjs"是全仓库唯一调用预算检查的地方,但没有任何东西去触发它;package.json 的"prepare": "turbo run build measure"只跑各包的 rollupmeasure任务,不跑检查。 packages/framer-motion/package.json:prepack 为"yarn build && yarn measure"(无检查);measure 为"rollup -c ./rollup.size.config.mjs"。packages/motion-dom/package.json:有"measure"(package.json)但完全没有"prepack"。.circleci/config.yml:现有setup/test/test-react/test-react-19/test-html五个任务。setup执行yarn install --immutable后yarn build并持久化整个 workspace;其余任务一律attach_workspace且requires: setup。不存在 measure/体积任务。全文件使用 4 空格 YAML 缩进(config.yml)。
基线实测数据(提交42bfbe3ed)
计划作者在基线提交上通过yarn build && yarn measure拿到了 9 条预算的实测对照表,这是理解"重校准"意义的直接证据:
| Bundle | 实测(kB gz) | 预算 | 状态 |
|---|---|---|---|
| framer-motion size-rollup-motion.js | 38.56 | 34.9 | ❌ |
| framer-motion size-rollup-m.js | 6.31 | 6 | ❌ |
| framer-motion size-rollup-dom-animation.js | 13.58 | 17.85 | ✅(过松) |
| framer-motion size-rollup-dom-max.js | 26.86 | 29.8 | ✅(过松) |
| framer-motion size-rollup-animate.js | 21.61 | 19.1 | ❌ |
| framer-motion size-rollup-scroll.js | 6.18 | 5.2 | ❌ |
| framer-motion size-rollup-waapi-animate.js | 3.15 | 2.26 | ❌ |
| motion-dom size-rollup-style-effect.js | 3.10 | 2.9 | ❌ |
| motion-dom size-rollup-motion-value.js | 1.70 | 1.8 | ✅ |
这张表暴露了两个方向的失真:一部分预算(motion、m、animate、scroll、waapi-animate、style-effect)因体积漂移而被击穿;另一部分(dom-animation、dom-max)则因历史原因"过松",宽松预算同样失去了约束意义。因此重校准的方向不是简单放水,而是所有条目一律收紧到当前实测水平。
度量链路是怎样工作的:从 size rollup 到 gzip 核对
要理解预算闸门,先要看清"数字从哪来"。仓库里存在两条前后衔接的链路:
第一步:按入口生成 size bundle。两个包的rollup.size.config.mjs定义了体积专用构建:
- framer-motion/rollup.size.config.mjs 声明了 7 个体积包,入口都指向
lib/下(tsc 编译产物)的真实源码入口,例如:motion:lib/render/components/motion/size.js→dist/size-rollup-motion.jsm:lib/render/components/m/size.js→dist/size-rollup-m.jsanimate:lib/animation/animate/index.jsscroll:lib/render/dom/scroll/index.jswaapi-animate:lib/animation/animators/waapi/animate-style.jsdom-animation/dom-max:以lib/render/dom/features-animation.js和features-max.js为多入口、共享 chunk 输出
- motion-dom/rollup.size.config.mjs 声明 2 个体积包:
lib/value/index.js→size-rollup-motion-value.js、lib/effects/style/index.js→size-rollup-style-effect.js。
所有 size bundle 共用同一套体积插件管线:resolve()(解析依赖)→replaceSettings("production")(生产环境替换)→terser(压缩去注释),并把react、react-dom、react/jsx-runtime列为 external。这模拟的是真实用户打包器(如 webpack)对该入口做生产构建时的产出规模,而非库内 dist 的原始体积。
第二步:预算脚本做 gzip 核对。bundlesize.mjs 对每个产物执行zlib.gzip(默认级别 ≈ level 6),计算 gzip 后字节数与maxSize(单位 kB,内部乘以 1024 换算字节)比较,超标输出❌ package/file is X kB (Y allowed)并累计失败,最终process.exit(1);未超标输出✅。该脚本的包选择逻辑见 bundlesize.mjs:无参数时检查["framer-motion", "motion-dom"]两个包,传参时只查单个包。
值得注意的细节:脚本的 gzip(zlib 默认级别)读数比 CLIgzip -9大约小 0.4%,因此预算必须锚定脚本的读数,而不是命令行 gzip 的读数——这是校准口径统一的关键(详见计划"维护说明")。
分步执行:从脚本改造到双链路接线
Plan 035 的全部改动收敛在 5 个文件范围内,其余文件即使"看起来相关"也明确列为 out of scope(例如两个包各自的rollup.size.config.mjs——测量本身没有问题;以及任何源码文件——本计划只改变流程不改变字节,体积回收由 Plan 036/037/038 负责)。
Step 1:让bundlesize.mjs与工作目录解耦
当前脚本用process.cwd()拼路径,导致它只能在仓库根目录运行。要让prepack阶段能在包目录下被调用,必须把路径解析改为基于脚本自身位置:
import { fileURLToPath } from "url" const repoRoot = fileURLToPath(new URL("../..", import.meta.url))在 bundlesize.mjs 顶部加入上述代码后,把两处process.cwd()(约第 15 行的packagePath拼接与约第 37 行的fullPath拼接)全部替换为repoRoot。
验证命令:从仓库根目录node dev/inc/bundlesize.mjs framer-motion应打印逐包体积表(预算尚未重校准,退出码 1 属预期);同时cd packages/framer-motion && node ../../dev/inc/bundlesize.mjs framer-motion必须打印同一张表——这证明了 cwd 无关性。
Step 2:把全部预算重校准到"当前实测 × 1.01"
在仓库根目录运行yarn build && yarn measure(measure 步骤会以退出码 1 结束——此时应当读取打印出的实测值)。对 framer-motion/package.json 和 motion-dom/package.json 中bundlesize数组的每一条,将maxSize设为"实测值 × 1.01,向上取整到最接近的 0.05 kB"。计划给出的预期落点:motion 39、m 6.4、dom-animation 13.75、dom-max 27.15、animate 21.85、scroll 6.25、waapi-animate 3.2、style-effect 3.15、motion-value 1.75。
这里有个工程上的重要约定:必须使用你自己构建产出的实测值,而不是计划表格里的数字——工具链噪声造成 ±0.05 kB 的波动完全正常,照抄历史表格反而会引入新的偏差。
验证命令:node dev/inc/bundlesize.mjs→ 全部 ✅,退出码 0。
Step 3:用 prepack 闸住发布流程
npm 在打包上传前会自动执行包的prepack脚本,这是发布链路中天然的"最后防线"。计划的接线方式:
packages/framer-motion/package.json的prepack改为:"prepack": "yarn build && yarn measure && node ../../dev/inc/bundlesize.mjs framer-motion"packages/motion-dom/package.json新增:"prepack": "yarn build && yarn measure && node ../../dev/inc/bundlesize.mjs motion-dom"
由于 Step 1 已经让脚本 cwd 无关,这条命令才能在包目录下工作。注意measure在此执行的是包的 rollup 体积构建,随后立刻被预算核对;任何一条预算超标都会让prepack以非零码失败,从而阻止yarn publish继续——体积失控的版本根本发不出去。
验证命令:cd packages/motion-dom && yarn prepack→ 退出码 0(先构建、后打印 ✅ 行);framer-motion 同理。
Step 4:新增阻塞性 CircleCImeasure任务
在 .circleci/config.yml 中仿照既有test任务(保持文件的 4 空格缩进)加入独立任务:
measure: docker: - image: cimg/node:20.11.1-browsers working_directory: ~/repo resource_class: large steps: - attach_workspace: at: ~/repo - run: name: Check bundle sizes command: yarn measure并在workflows: build: jobs:下挂载依赖:
- measure: requires: - setup这里的关键设计:setup任务在执行yarn build后持久化了整个 workspace,lib/(tsc 产物,size rollup 的消费输入)已经就位;因此measure任务只需attach_workspace并执行yarn measure——它只重跑 size rollup 加预算核对,无需重新全量构建,耗时可控。任务独立成 job 而非并入test,是为了让"体积回归"这一信号在 CI 面板上单独可见、失败定位清晰(计划的维护说明也提到:若 CircleCI 分钟数成为顾虑,可将其折叠进test任务作为额外步骤,只是信号清晰度会下降)。
验证命令:python3 -c "import yaml; yaml.safe_load(open('.circleci/config.yml'))"→ 退出码 0,确认 YAML 语法合法。
执行纪律:测试计划、完成标准与 STOP 条件
作为一份面向执行者的操作计划,Plan 035 定义了明确的质量闭环:
测试计划:本计划属于构建/流程类工具链改造,不涉及单元测试——每一步的验证命令本身就是测试。最终端到端检查:在干净的(无git stash残留)工作树上运行yarn build && yarn measure→ 退出码 0、每一行 ✅。
完成标准(全部可机器核验):
yarn measure退出码 0 且全部行 ✅;cd packages/framer-motion && node ../../dev/inc/bundlesize.mjs framer-motion退出码 0(cwd 无关性);grep -c "bundlesize.mjs" packages/framer-motion/package.json packages/motion-dom/package.json各返回 1(prepack 已接线);grep -c "measure:" .circleci/config.yml≥ 1 且 YAML 可解析;git status确认没有超出 in-scope 清单的文件被改动;- plans/README.md 中的状态行已更新。
STOP 条件(触发即停止上报,不得自行发挥):
- 改动任何东西之前
yarn build就失败——说明基线已损坏,重校准会把垃圾数字编码进预算; - 某个实测体积与计划表格偏差超过 1 kB gz(双向)——说明计划与执行之间存在其他落地变更(如
cleanup/strip-unused-stats分支或 Plan 036/037),此时应重读plans/README.md、针对新现实重新校准并记录; - Plan 007 已对同一个
workflows:块落地冲突改动且合并不是机械式——需要人工仲裁。
预算即棘轮:这套机制的长期意义
计划在"维护说明"中给出了这套治理体系最核心的长期契约:
- 预算从此成为棘轮(ratchet)。后续的 Plan 036/037/038(分别针对 dev 警告 DCE、scale corrector 泄漏、motion-dom 重模块减重)每一步收尾时都会重新收紧它们所改善的预算;任何合法地让某个包变大的 PR,必须在同一提交内上调对应预算——这一行 diff 就是本计划存在的意义:它是评审者识别体积增长的第一信号。
- 重校准的数值并不代表"背书"。重新校准为主 motion 包带来了约 3.7 kB gz 的历史漂移豁免,这笔债由 Plan 036/037/038 负责偿还,新预算不应被当作被认可的目标体积。
- gzip 口径差异:脚本 zlib 默认级别(≈ level 6)读数比
gzip -9小约 0.4%,预算以脚本为准、不以 CLI 为准。 - CI 成本的折中:
measure独立成 job 是为了信号清晰;若 CircleCI 分钟数紧张,可合并进test任务。
此外,仓库中与体积治理同级的既有防线还包含 check-bundle.js,它负责类型导出自包含、CJSm包不内联自己的LazyContext等结构性校验——体积预算闸门与这些类型/结构闸门共同构成 framer-motion 发布前的完整质量网。
结语
Plan 035 的完整价值可以概括为一句工程哲学:度量本身不是治理,只有把度量变成不可绕过的关卡,治理才算生效。它通过四个精确步骤——脚本 cwd 无关化、预算重校准(实测 ×1.01 向上取整)、prepack 发布闸、CircleCI 阻塞任务——把 motion 仓库从"6/9 预算静默失败"修复为"任何体积回归都在合并或发布前被机器强制拦截",并以棘轮契约保证这条防线在未来的每一次体积优化中持续收紧而非松弛。对任何以"体积即特性"为卖点的库而言,这套从工具、校准、接线到纪律的完整范式都值得直接借鉴。
- 前端
- UI组件
【免费下载链接】motion
A modern animation library for React and JavaScript
相关推荐
Front-End-Checklist 性能预算实战:用 size-limit 与 Lighthouse CI 在 CI 中强制执行
Front End Checklist 性能预算实战:用 size limit 与 Lighthouse CI 在 CI 中强制执行 性能预算是作用于可度量指标
OpenHuman 成本追踪与预算强制模块解析:JSONL 用量记账、每日/月度预算闸门与 7 天成本看板
OpenHuman 成本追踪与预算强制模块解析:JSONL 用量记账、每日/月度预算闸门与 7 天成本看板 OpenHuman 是一个面向 Mac、Window
人工智能AI 应用本地部署AI Agent交互助手深度研究gh_mirrors/we/webpack项目的性能预算:bundle体积控制策略
gh_mirrors/we/webpack项目的性能预算:bundle体积控制策略 1. 性能预算:前端工程的隐形红线 你是否曾遇到过这样的困境:开发环境流畅的
前端示例工程前端构建
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考