小程序主包 2MB 卡了三个月,我才发现体积大头根本不是图片

🔑 关键词:小程序分包,主包2M限制,小程序包体积优化,独立分包,分包异步化

📖 摘要:用 Taro 3.6 做的二手书小程序,主包 1.98MB 编译失败。压完 43 张图只省 110KB,真正的大头是 core-js polyfill 392KB、全量引入的组件库 287KB,还有一个早被砍掉的需求留下的图表库。这篇把代码依赖分析、分包、独立分包、分包异步化、preloadRule 这几个坑按我踩的顺序讲一遍。

开头先说个让人不太舒服的结论

图片

小程序主包 2MB 这堵墙,大部分团队撞上去的第一反应是压图片,然后发现忙活两天只省下一百来 KB。真正占体积的是三样东西:编译注入的 polyfill、全量引入的组件库、还有躺在 node_modules 里没人敢删的历史依赖。我们那个项目从 1.98MB 压到 812KB,图片总共只贡献了 40KB 左右,剩下全部是删代码删出来的。

所以下面这套流程,我建议你把顺序倒过来做:先看依赖分析,再动分包,最后才去碰图片。


一、三周写完,被一个红条拦住

2023 年 7 月我接了个二手书交易的小程序,甲方要求十一之前上线。技术栈是上一任外包定的 Taro 3.6 + React,很典型的配置,代码目录一打开就知道是模板改的。功能不复杂:首页、分类、详情、下单、个人中心,五个 tabBar 页面加十来个二级页。我前前后后写了三周,本地跑着挺顺,直到提交体验版那天,开发者工具弹了红条——主包体积 1.98MB,超出 2MB 限制,编译直接失败。

图片

那一刻我打开 assets 文件夹,第一个念头是:图肯定太多了。事后想想这个判断挺蠢的,但当时确实就是这么想的。

二、压完图我懵了

第一轮优化基本白干。我把 assets 里 43 张 png 全转成 webp,又丢进 tinypng 压了一遍,最大的三张 banner 从 180KB 压到 42KB。折腾到凌晨两点,主包从 1.98MB 降到 1.87MB,只省了 110KB。

第二天我换了个思路,点开微信开发者工具的「详情 - 本地代码 - 代码依赖分析」,那一屏数字我记到现在:

  • 图片资源合计:214KB
  • core-js 相关 polyfill:392KB
  • @tarojs/components 全量引入:287KB
  • 一个叫 echarts-for-weixin 的包:156KB
  • 其余是业务代码和 node_modules 里的零碎

最后那个图表库最离谱。它只在「年度账单」页面出现过一次,而那个页面因为需求变更早就被砍了,入口没了,依赖还挂在 package.json 里。我翻 git log 查了半天,是上一任在项目第一天装的,之后再没人动过。

图片

三、同一个页面,三种技术栈的编译体积对比

为了给甲方解释为什么要改架构,我拿同一个「图书详情页」在三种方案下各编了一次,用的是同一份设计稿、同样的接口数据:

方案 单页面主包增量 首屏渲染耗时(中端安卓,4G) 备注
微信原生 约 62KB 410ms 无运行时
Taro 3.6(React) 约 214KB 690ms 含运行时 + polyfill
uni-app(Vue3) 约 148KB 580ms 运行时略轻

这组数字不是说原生一定更好,原生写五个 tabBar 加十几个页面,人力成本能翻一倍。我想说的是:跨端框架的「体积税」是真实存在的,大约在 150KB 到 250KB 之间,你得提前把这笔账算进 2MB 的预算里,而不是等它编译失败那天才发现。

四、分包这件事,很多人第一步就配错了

图片

主包和单个分包各自不能超过 2MB,这个大家都知道。但真正容易翻车的是「什么该放主包」。

我见过太多项目把 tabBar 之外的页面一股脑扔进分包,结果主包里还留着整个组件库。判断标准其实很简单:主包只放启动路径上一定会执行的东西——tabBar 页面、全局状态、登录逻辑、请求封装。其余全部推给分包。

我们最终的配置大概是这样的:

{
  "pages": [
    "pages/index/index",
    "pages/category/index",
    "pages/cart/index",
    "pages/order/index",
    "pages/mine/index"
  ],
  "subPackages": [
    { "root": "packageBook", "pages": ["detail/index", "comment/index"] },
    { "root": "packageOrder", "pages": ["confirm/index", "pay/index"] },
    { "root": "packageUser", "pages": ["setting/index", "address/index"] },
    { "root": "packageTool", "pages": ["scan/index"], "independent": true }
  ],
  "preloadRule": {
    "pages/index/index": { "network": "all", "packages": ["packageBook"] },
    "pages/category/index": { "network": "wifi", "packages": ["packageOrder"] }
  }
}

改完这一版,主包落到 1.1MB 左右。注意 network 那个字段,值只有 allwifi 两种,写成 4g 是不生效的,而且它是静默失效——不报错,你只会觉得分包怎么没预下载。这个坑我当时查了半小时文档才反应过来。

图片

五、独立分包和分包异步化,用错地方会更慢

这两个特性经常被混着讲,但其实面向的场景完全不同。

独立分包(independent: true) 适合那种「用户从分享卡片直接进来」的页面,比如扫码结果页、活动落地页。它不依赖主包就能启动,代价是拿不到主包里的全局状态和自定义组件。我们那个扫码页放进独立分包后,冷启动从 1.4s 降到 0.9s 左右,但代价是我得把登录逻辑在分包里重写一份,大概多写了 120 行代码。

分包异步化 解决的是另一个问题:A 分包想用 B 分包里的组件。做法是在 usingComponents 里加 componentPlaceholder,再配合 require 跨分包引用。听起来很美好,实际上它会让组件先渲染一个占位,等分包下载完再替换,视觉上会有一次跳变。列表页里用这个,用户滑动时能看到明显的闪一下。

我的经验是:分包异步化只适合低频、且位置固定的组件,比如详情页底部的推荐模块。列表项里千万别用。

六、一份可以直接抄的排查步骤

图片

如果你现在正卡在 2MB,按这个顺序走一遍,大概率能救回来:

  1. 打开开发者工具「详情 - 本地代码 - 代码依赖分析」,先截图存一份,这是你的基线。
  2. 在依赖列表里按体积排序,凡是超过 100KB 且你叫不上名字的包,先去 package.json 里搜它被谁引用了。我们那个 echarts 就是这么揪出来的。
  3. 检查 babel 配置里的 useBuiltIns。Taro 默认会注入相当一批 core-js,如果你的基础库版本要求已经比较新,很多 polyfill 是可以砍掉的。这一步我们省了 300 多 KB。
  4. 组件库改成按需引入。@tarojs/components 全量引入 287KB,按需之后大概 90KB。
  5. 以上做完再看图片,webp + 合适的尺寸,这一步的收益通常不超过 100KB。
  6. 最后才做分包,并且一定要配 preloadRule,network 记得写 all

最后说点感受

这套流程我后来又用在两个项目上,最快的一次半天就搞定了。但我始终觉得,2MB 这个数字真正卡住的不是技术,是习惯——大家习惯了「先装上再说」,习惯了不敢删依赖,习惯了把优化留到编译失败那天。

我现在的做法是,新项目第一天就在 CI 里加一条主包体积检查,超过 1.6MB 直接报警。提前知道,和上线前一天知道,完全是两种心情。

🏷️ 标签: