微信主包 2MB 硬限,是每个小程序开发者都绕不开的一道墙。这篇文章记录 HealthLoop(一个 AI 健康记录小程序)两次被这道墙打回来的完整过程,以及最后沉淀下来的治理方法。

第一次撞墙:2393KB 超限,元凶不是业务代码

上传体验版时,开发者工具报 source size 2393KB exceed max limit 2MB。外层错误写的是"网络请求错误",但反复重试不会恢复——这类错误信息经常误导排查方向。

逐层统计 miniprogram/ 目录后,构成是这样的:

  • 业务源码:约 1.11MB
  • 图片资源:约 80KB
  • TDesign 组件生成包:大头

真正的账本在 miniprogram_npm/tdesign-miniprogram 里:图标字体已经以内嵌 Base64 写进 icon.wxss,目录里却还留着约 273KB 的原始字体;另有约 75KB 的 IDE 构建元数据。之前只解决了远程字体加载失败,没有把"本地字体如何进入上传包"纳入体积预算。而微信构建 npm 会恢复完整组件目录——手动删一次,下次构建又回来,协作者也无法稳定复现。

解法是自动化:写一个统一的 npm 准备脚本,在 npm ci 后精简 TDesign 依赖源,只保留实际组件的依赖闭包,移除 .d.ts、重复字体和 IDE 元数据;代码包检查加入 npm run check,防止新增依赖后再次静默超限。

顺带还抓出一个隐藏雷:训练草稿的失败提示用了当前 TDesign 版本里不存在的 warning-circle 图标,改成有效的 error-circle,并让准备脚本校验所有静态 t-icon 名称——避免瘦身后才暴露空白图标。

第二次撞墙:预算早已 99.3% 耗尽

几个月后,新增一个 34KB 的二级页,检查立刻失败:2067.1 KiB; expected under 2048 KiB。而改动前主包已经是 2033.3KB——红线的 99.3%,只剩 15KB 余量。这根红线本质上是长期贴线运行,任何新功能都会撞上。

逐层拆字节构成,找到四笔"隐形账单":

  1. 死依赖随包发布:TDesign 生成包里留着 marked(38.8KiB)、dayjs(19.9KiB)、tinycolor2(16.7KiB)。它们是日期选择器、颜色选择器、chat 组件的依赖——而那批组件早已被裁掉。保留组件的 import 闭包只引用 tslib。
  2. 不可达共享模块:common/shared/color-picker(8.7KiB)全树零引用,它的唯一消费者是被裁掉的组件。
  3. 从不渲染的亮色资源副本:WXML 硬编码暗色路径,29.2KiB 的亮色 PNG 永远不被引用。
  4. 文档混进上传包:图标目录里的 README 和许可证文件占着运行包的空间。

共同根因一句话:体积裁剪规则按"目录名单"写,而不是按"运行时引用闭包"写。白名单只在一层组件目录上做,没有下探嵌套依赖和共享子模块,于是每次上游升级都把死重量带回来,检查却一路绿灯。

修复原则

  1. 打包脚本下探两层:miniprogram_npm/ 只保留 tslib,common/shared/ 剔除死模块;--check 模式对两层同样断言"无多余条目",死依赖回归即红灯。
  2. 上传包不是仓库:文档、许可证移出包体(换个位置就行,别为了省 KB 删许可证)。
  3. 不抬阈值:2048KiB 是平台硬限,守卫是最后一道防线,不是可以协商的数字。

结果:主包从 2067.1KiB 降到 1949.5KiB,余量回升到 98.5KiB。

四条教训

  • 看体积先看余量。绿色但 99.3% 占用的检查,等于把一次性配额当成常备余量。
  • 白名单裁剪必须锚定运行时引用闭包(import 关系 + usingComponents),目录级名单挡不住传递依赖;上游每次升级都是一次"死重量回填"。
  • 大文件 Top 榜是最快的排查入口:第一名(内嵌字体 369KiB)没法动,但第二梯队里的死依赖和死资源,往往一查一个准。
  • 修复超限必须自动化并进回归,不能依赖某位开发者记得手工清目录。