微信小程序分包加载与体积控制的7个技巧

小程序主包限制 2MB,总包限制 20MB。超过限制意味着无法发布或无法正常使用。本文从分包策略到体积优化,给出一套完整的工程化方案。

一、体积限制与超限后果

微信对小程序体积有严格限制:

限制类型 限制值 超限后果
主包大小 2MB 无法预览、无法上传
总包大小(主包+所有分包) 20MB 无法上传
单个分包大小 不超过主包限制 上传时报错
微信小游戏 4MB(主包) 无法上传

超出限制时的常见表现:

code复制

复制代码
[上传]错误:包体积超过限制
主包大小 2.3MB,超过限制 2MB
请通过分包或裁剪不必要代码后重试

排查体积的第一步------查看各包大小:

bash复制

复制代码
# 在开发者工具中查看
# 菜单栏 → 详情 → 基本信息 → 代码包大小

也可以用脚本自动统计:

javascript复制

复制代码
// scripts/analyze-size.js
const fs = require('fs')
const path = require('path')

function getDirSize(dirPath) {
  let totalSize = 0

  const items = fs.readdirSync(dirPath)
  items.forEach(item => {
    const itemPath = path.join(dirPath, item)
    const stat = fs.statSync(itemPath)

    if (stat.isDirectory()) {
      totalSize += getDirSize(itemPath)
    } else {
      totalSize += stat.size
    }
  })

  return totalSize
}

function analyzePackage(rootDir, packageName) {
  const sizeInBytes = getDirSize(rootDir)
  const sizeInKB = (sizeInBytes / 1024).toFixed(2)
  const sizeInMB = (sizeInBytes / 1024 / 1024).toFixed(4)

  console.log(`📦 ${packageName}: ${sizeInKB} KB (${sizeInMB} MB)`)

  // 列出各子目录大小
  const subDirs = fs.readdirSync(rootDir)
    .filter(item => fs.statSync(path.join(rootDir, item)).isDirectory())
    .sort((a, b) => getDirSize(path.join(rootDir, b)) - getDirSize(path.join(rootDir, a)))

  subDirs.forEach(dir => {
    const dirSize = getDirSize(path.join(rootDir, dir))
    console.log(`  └── ${dir}: ${(dirSize / 1024).toFixed(2)} KB`)
  })
}

// 分析主包
analyzePackage('./miniprogram', '主包')

// 分析分包
const subpackages = ['subpkg-order', 'subpkg-user', 'subpkg-promotion']
subpackages.forEach(pkg => {
  const pkgPath = path.join('./miniprogram', pkg)
  if (fs.existsSync(pkgPath)) {
    analyzePackage(pkgPath, `分包: ${pkg}`)
  }
})

// 检查是否超限
const mainPkgSize = getDirSize('./miniprogram')
if (mainPkgSize > 2 * 1024 * 1024) {
  console.warn(`⚠️ 主包超限!当前 ${(mainPkgSize / 1024 / 1024).toFixed(4)}MB,限制 2MB`)
}

二、技巧1:科学的分包策略设计

分包不是把页面随便拆一拆就完事了。好的分包策略应该基于以下几个维度来设计。

2.1 按功能模块分包

最直观的分包方式------把不同功能模块的页面放到不同分包中:

json复制

复制代码
{
  "pages": [
    "pages/index/index",
    "pages/search/search"
  ],
  "subpackages": [
    {
      "root": "subpkg-order",
      "name": "order",
      "pages": [
        "pages/list/list",
        "pages/detail/detail",
        "pages/confirm/confirm",
        "pages/result/result"
      ]
    },
    {
      "root": "subpkg-user",
      "name": "user",
      "pages": [
        "pages/profile/profile",
        "pages/settings/settings",
        "pages/address/list/list",
        "pages/address/edit/edit"
      ]
    },
    {
      "root": "subpkg-marketing",
      "name": "marketing",
      "pages": [
        "pages/coupon/coupon",
        "pages/points/points",
        "pages/share/share"
      ]
    }
  ]
}

适用场景: 中大型小程序,功能边界清晰。

2.2 按业务场景分包

根据用户使用路径来分包,把同一场景下连续访问的页面放在一起:

json复制

复制代码
{
  "subpackages": [
    {
      "root": "subpkg-onboarding",
      "pages": [
        "pages/welcome/welcome",
        "pages/auth/auth",
        "pages/profile-setup/profile-setup",
        "pages/interest/interest"
      ]
    },
    {
      "root": "subpkg-checkout",
      "pages": [
        "pages/cart/cart",
        "pages/checkout/checkout",
        "pages/payment/payment",
        "pages/success/success"
      ]
    }
  ]
}

适用场景: 有明确用户漏斗的小程序,如电商、注册流程。

2.3 按页面频率分包

高频页面放在主包,中频页面放在常预加载的分包,低频页面放在按需加载的分包:

json复制

复制代码
{
  "pages": [
    "pages/index/index",
    "pages/category/category",
    "pages/cart/cart"
  ],
  "subpackages": [
    {
      "root": "subpkg-common",
      "pages": [
        "pages/search/search",
        "pages/product/product",
        "pages/shop/shop"
      ]
    },
    {
      "root": "subpkg-rare",
      "pages": [
        "pages/about/about",
        "pages/help/help",
        "pages/feedback/feedback",
        "pages/agreement/agreement"
      ]
    }
  ]
}

适用场景: 页面访问频率差异明显的小程序。

三、技巧2:分包预下载最佳实践

分包加载的目的是减小首包体积,但如果用户跳转到分包页面时需要下载,体验会很差。预下载可以解决这个问题。

3.1 preloadRule 配置

json复制

复制代码
{
  "preloadRule": {
    "pages/index/index": {
      "network": "all",
      "packages": ["__APP__"]
    },
    "pages/category/category": {
      "network": "wifi",
      "packages": ["subpkg-common"]
    },
    "pages/cart/cart": {
      "network": "all",
      "packages": ["subpkg-checkout"]
    }
  }
}

参数详解:

  • network: "all" 在所有网络下预下载,"wifi" 仅在 WiFi 下预下载
  • packages: 分包的 root 名称数组
  • __APP__: 特殊值,表示预下载所有分包(谨慎使用)

3.2 预下载策略建议

javascript复制

复制代码
// 在代码中监听预下载状态
wx.onSubpackageDownload && wx.onSubpackageDownload((res) => {
  console.log('分包预下载状态:', res)
})

// 主动触发分包下载(不等页面跳转就提前下载)
wx.loadSubpackage({
  root: 'subpkg-checkout',
  success() {
    console.log('分包下载成功')
    // 可以提前初始化分包数据
  },
  fail(err) {
    console.error('分包下载失败:', err)
    // 降级处理:提示用户或重试
  }
})

最佳实践清单:

页面 预下载分包 network 原因
首页 核心1-2个分包 all 首页是流量入口,用户大概率会继续浏览
列表页 详情页所在分包 all 列表→详情是高频路径
购物车 结算页分包 wifi 结算流程不急迫,WiFi下预下载即可
个人中心 低频分包 wifi 低优先级,避免消耗用户流量

注意: 预下载会在微信空闲时执行,不会阻塞当前页面渲染。但如果一次配置太多分包预下载,可能会导致网络资源争抢。建议单页预下载不超过2个分包。

四、技巧3:独立分包的使用

独立分包是可以独立于主包运行的分包。用户进入独立分包页面时不需要下载主包,特别适合从外部场景(扫码、分享卡片)直接进入特定功能页。

4.1 独立分包配置

json复制

复制代码
{
  "subpackages": [
    {
      "root": "subpkg-standalone",
      "name": "standalone",
      "independent": true,
      "pages": [
        "pages/scan-result/scan-result",
        "pages/quick-pay/quick-pay"
      ]
    }
  ]
}

4.2 独立分包中的代码约束

独立分包不能依赖主包中的代码(包括 app.js 中的全局数据、主包的公共组件等)。需要在独立分包内部做好自包含:

javascript复制

复制代码
// subpkg-standalone/app-service.js
// 独立分包内部的全局服务,替代主包 app.js 中的逻辑

let globalData = {
  userInfo: null,
  token: '',
  systemInfo: null
}

function init() {
  // 独立分包初始化时执行
  try {
    const info = wx.getDeviceInfo()
    globalData.systemInfo = info

    // 尝试从缓存读取用户信息
    const cachedUser = wx.getStorageSync('userInfo')
    if (cachedUser) {
      globalData.userInfo = cachedUser
    }
  } catch (e) {
    console.error('初始化失败:', e)
  }
}

function getGlobalData() {
  return globalData
}

module.exports = { init, getGlobalData }

javascript复制

复制代码
// subpkg-standalone/pages/scan-result/scan-result.js
const appService = require('../../app-service.js')

Page({
  onLoad(options) {
    // 独立分包页面的 onLoad 中初始化
    appService.init()

    const globalData = appService.getGlobalData()
    console.log('系统信息:', globalData.systemInfo)

    // 处理扫码进入的参数
    if (options.q) {
      const decodedUrl = decodeURIComponent(options.q)
      this.handleScanResult(decodedUrl)
    }
  },

  handleScanResult(url) {
    // 处理扫码结果
    console.log('扫码内容:', url)
  }
})

4.3 独立分包跳转主包

javascript复制

复制代码
// 独立分包中跳转到主包页面
Page({
  goToHome() {
    // 需要使用 reLaunch,因为独立分包不依赖主包
    wx.reLaunch({
      url: '/pages/index/index',
      success: () => {
        console.log('跳转到主包首页')
      },
      fail: (err) => {
        console.error('跳转失败:', err)
        // 可能主包还没下载完,给用户提示
        wx.showToast({
          title: '正在加载,请稍候',
          icon: 'loading'
        })
      }
    })
  }
})

适用场景: 扫码进入支付页、分享卡片打开特定活动页、外部链接跳转到功能页。

五、技巧4:分包异步化(跨包调用组件)

小程序从基础库 2.11.1 开始支持分包异步化,允许分包引用其他分包或主包的组件,而不需要把公共组件复制到每个分包中。

5.1 配置分包异步化

json复制

复制代码
{
  "subpackages": [
    {
      "root": "subpkg-order",
      "pages": ["pages/list/list"]
    },
    {
      "root": "subpkg-user",
      "pages": ["pages/profile/profile"]
    }
  ],
  "usingComponents": {
    "shared-card": "/components/shared-card/shared-card"
  }
}

5.2 跨分包引用组件

html复制

复制代码
<!-- subpkg-order/pages/list/list.wxml -->
<!-- 引用主包中的组件 -->
<shared-card data="{{item}}" bindtap="onCardTap" />

<!-- 引用其他分包中的组件(需要分包异步化) -->
<view wx:if="{{showUserInfo}}">
  <user-card
    wx:if="{{loaded}}"
    user="{{userInfo}}"
  />
</view>

javascript复制

复制代码
// subpkg-order/pages/list/list.js
Page({
  data: {
    loaded: false,
    userInfo: null
  },

  async onLoad() {
    // 异步加载其他分包的组件
    const { getUserCardComponent } = require('./async-components')
    const userCard = await getUserCardComponent()
    this.setData({ loaded: true })
  }
})

5.3 分包异步化的回调占位

分包异步化加载需要时间,加载完成前需要给用户一个占位视图:

javascript复制

复制代码
// 使用 wx.require 异步 require
Page({
  data: {
    componentReady: false
  },

  onLoad() {
    // 异步 require 其他分包的模块
    this.requireAsync('subpkg-user/utils/user-service.js').then(module => {
      this.userService = module
      this.setData({ componentReady: true })
    })
  },

  requireAsync(path) {
    return new Promise((resolve, reject) => {
      wx.require(path, (module) => {
        if (module) {
          resolve(module)
        } else {
          reject(new Error(`加载模块失败: ${path}`))
        }
      })
    })
  }
})

html复制

复制代码
<view wx:if="{{!componentReady}}" class="loading-placeholder">
  <view class="skeleton"></view>
  <text>加载中...</text>
</view>

<user-card wx:if="{{componentReady}}" user="{{userInfo}}" />

六、技巧5:静态资源体积优化

代码体积中占大头的往往是静态资源------图片、字体、图标。

6.1 图片压缩与格式转换

javascript复制

复制代码
// build-scripts/compress-images.js
// 构建脚本:自动压缩图片并转换为 WebP
const { execSync } = require('child_process')
const fs = require('fs')
const path = require('path')

// 使用 tinypng CLI 压缩图片
function compressWithTinypng(dir) {
  const images = findImages(dir)
  images.forEach(img => {
    execSync(`tinypng ${img} --key YOUR_TINYPNG_KEY`)
    console.log(`压缩完成: ${img}`)
  })
}

// 使用 cwebp 转换为 WebP
function convertToWebp(dir, quality = 80) {
  const images = findImages(dir, ['.png', '.jpg', '.jpeg'])
  images.forEach(img => {
    const webpPath = img.replace(/\.(png|jpg|jpeg)$/i, '.webp')
    execSync(`cwebp -q ${quality} ${img} -o ${webpPath}`)
    // 删除原文件
    fs.unlinkSync(img)
    console.log(`转换完成: ${img} → ${webpPath}`)
  })
}

function findImages(dir, exts = ['.png', '.jpg', '.jpeg', '.gif']) {
  const results = []
  const items = fs.readdirSync(dir)
  items.forEach(item => {
    const itemPath = path.join(dir, item)
    const stat = fs.statSync(itemPath)
    if (stat.isDirectory()) {
      results.push(...findImages(itemPath, exts))
    } else if (exts.includes(path.extname(item).toLowerCase())) {
      results.push(itemPath)
    }
  })
  return results
}

// 执行
compressWithTinypng('./miniprogram/images')
convertToWebp('./miniprogram/images', 80)

压缩效果对比:

格式 原始大小 压缩后 压缩率
PNG → PNG (tinypng) 500KB 180KB 64%
PNG → WebP 500KB 95KB 81%
JPG → WebP 300KB 75KB 75%

6.2 字体子集化

小程序中引入自定义字体文件时,完整字体通常有几 MB。但实际上你只需要用到几十个汉字。使用字体子集化工具只提取需要的字符:

javascript复制

复制代码
// build-scripts/subset-font.js
// 使用 fontmin 提取需要的字符
const Fontmin = require('fontmin')

// 从代码中提取所有用到的文字
const usedChars = extractUsedChars('./miniprogram')

new Fontmin()
  .src('./assets/fonts/custom-font.ttf')
  .dest('./miniprogram/assets/fonts')
  .use(Fontmin.glyph({
    text: usedChars,
    hinting: false
  }))
  .use(Fontmin.ttf2woff())  // 同时转 woff 格式
  .run((err, files) => {
    if (err) throw err
    console.log('字体子集化完成')
    files.forEach(f => {
      const size = fs.statSync(f.path).size
      console.log(`${f.path}: ${(size / 1024).toFixed(2)} KB`)
    })
  })

function extractUsedChars(dir) {
  let text = ''
  const items = fs.readdirSync(dir)
  items.forEach(item => {
    const itemPath = path.join(dir, item)
    const stat = fs.statSync(itemPath)
    if (stat.isDirectory()) {
      text += extractUsedChars(itemPath)
    } else if (/\.(wxml|wxss|js|json)$/.test(item)) {
      text += fs.readFileSync(itemPath, 'utf-8')
    }
  })
  // 去重
  return [...new Set(text)].join('')
}

css复制

复制代码
/* app.wxss */
@font-face {
  font-family: 'CustomFont';
  src: url('./assets/fonts/custom-font.woff') format('woff');
}

.custom-font {
  font-family: 'CustomFont';
}

6.3 代码压缩与冗余清理

javascript复制

复制代码
// webpack.config.js 或 build.js 中配置
// 使用 TerserPlugin 压缩 JS
const TerserPlugin = require('terser-webpack-plugin')

module.exports = {
  mode: 'production',
  optimization: {
    minimizer: [
      new TerserPlugin({
        terserOptions: {
          compress: {
            drop_console: true,  // 移除 console
            drop_debugger: true,
            pure_funcs: ['console.log']
          },
          output: {
            comments: false
          }
        }
      })
    ]
  }
}

npm 包精简: 小程序中使用 npm 时,整个包会被打包进代码。按需引入可以大幅减少体积:

javascript复制

复制代码
// ❌ 引入整个 lodash
const _ = require('lodash')
_.get(obj, 'a.b.c')

// ✅ 只引入需要的函数
const get = require('lodash/get')
get(obj, 'a.b.c')

// ❌ 引入整个 moment.js(280KB+)
const moment = require('moment')
moment(timestamp).format('YYYY-MM-DD')

// ✅ 使用轻量替代
// dayjs 只有 2KB
const dayjs = require('dayjs')
dayjs(timestamp).format('YYYY-MM-DD')

// 或者直接写格式化函数
function formatDate(timestamp, fmt = 'YYYY-MM-DD') {
  const d = new Date(timestamp)
  const map = {
    YYYY: d.getFullYear(),
    MM: String(d.getMonth() + 1).padStart(2, '0'),
    DD: String(d.getDate()).padStart(2, '0'),
    HH: String(d.getHours()).padStart(2, '0'),
    mm: String(d.getMinutes()).padStart(2, '0'),
    ss: String(d.getSeconds()).padStart(2, '0')
  }
  return fmt.replace(/YYYY|MM|DD|HH|mm|ss/g, m => map[m])
}

七、技巧6:静态资源CDN托管

将大文件资源从代码包中移出,托管到 CDN 上,是最有效的体积优化手段之一。

7.1 图片CDN化

javascript复制

复制代码
// config/cdn-config.js
const CDN_BASE = {
  production: 'https://cdn.yourdomain.com/miniprogram',
  develop: 'https://cdn-dev.yourdomain.com/miniprogram'
}

const env = __wxConfig.envVersion || 'release'
const baseUrl = CDN_BASE[env === 'release' ? 'production' : 'develop']

// 图片资源映射表
const imageMap = {
  'logo': '/images/logo.png',
  'banner-home': '/images/banner-home.png',
  'icon-cart': '/images/icon-cart.png',
  'icon-user': '/images/icon-user.png',
  'empty-list': '/images/empty-list.png'
}

function cdnImage(key, params = {}) {
  const { w, h, q, format } = params
  let url = `${baseUrl}${imageMap[key] || key}`

  // 拼接图片处理参数(七牛/阿里云OSS等CDN服务支持)
  const queries = []
  if (w) queries.push(`imageView2/2/w/${w}`)
  if (h) queries.push(`h/${h}`)
  if (q) queries.push(`q/${q}`)
  if (format) queries.push(`format/${format}`)
  if (queries.length) url += '?' + queries.join('/')

  return url
}

module.exports = { cdnImage, CDN_BASE }

html复制

复制代码
<!-- 使用 -->
<image src="{{cdn.logo}}" mode="aspectFit" />
<image src="{{cdn.banner}}" mode="aspectFill" />

javascript复制

复制代码
const { cdnImage } = require('../../config/cdn-config')

Page({
  data: {
    cdn: {
      logo: cdnImage('logo', { w: 200, format: 'webp' }),
      banner: cdnImage('banner-home', { w: 750, h: 400, q: 80, format: 'webp' })
    }
  }
})

7.2 配置 downloadFile 合法域名

CDN 域名需要在小程序管理后台配置为合法下载域名:

code复制

复制代码
小程序管理后台 → 开发管理 → 开发设置 → 服务器域名 → downloadFile合法域名
添加:https://cdn.yourdomain.com

八、技巧7:常见分包踩坑与避坑指南

8.1 分包路径踩坑

json复制

复制代码
// ❌ 错误:分包 root 路径重复
{
  "subpackages": [
    { "root": "subpkg", "pages": ["subpkg/pages/list/list"] }
  ]
}
// 分包 root 是 "subpkg",页面路径应该是 "pages/list/list",不是 "subpkg/pages/list/list"

// ✅ 正确
{
  "subpackages": [
    { "root": "subpkg", "pages": ["pages/list/list"] }
  ]
}

json复制

复制代码
// ❌ 错误:分包 root 和主包页面路径冲突
{
  "pages": ["pages/index/index"],
  "subpackages": [
    { "root": "pages", "pages": ["order/list/list"] }
  ]
}
// "pages" 既是主包目录又是分包 root,会冲突

// ✅ 正确:分包 root 使用独立目录
{
  "pages": ["pages/index/index"],
  "subpackages": [
    { "root": "subpkg-order", "pages": ["pages/list/list"] }
  ]
}

8.2 跨分包跳转限制

javascript复制

复制代码
// ❌ 使用 navigateTo 跳转到分包页面有时会失败
wx.navigateTo({
  url: '/subpkg-order/pages/detail/detail?id=123'
})

// 分包页面还没下载时,navigateTo 会自动下载分包再跳转
// 但如果分包较大,用户会看到一段空白等待期

// ✅ 添加 loading 提示
wx.navigateTo({
  url: '/subpkg-order/pages/detail/detail?id=123',
  success: () => {
    wx.hideLoading()
  },
  fail: (err) => {
    wx.hideLoading()
    wx.showToast({ title: '页面加载失败', icon: 'error' })
    console.error('跳转失败:', err)
  }
})

// 跳转前显示 loading
wx.showLoading({ title: '加载中...', mask: true })

// ✅ 更好的做法:提前预下载分包
wx.loadSubpackage({
  root: 'subpkg-order',
  success: () => {
    console.log('分包已下载,跳转将秒开')
  }
})

8.3 wx.navigateTo 层级限制

小程序中 navigateTo 最多保留 10 层页面栈。超过后无法继续跳转:

javascript复制

复制代码
// 跨分包跳转时尤其要注意页面栈深度
Page({
  goDetail() {
    const pages = getCurrentPages()
    console.log(`当前页面栈深度: ${pages.length}`)

    if (pages.length >= 8) {
      // 接近上限,使用 redirectTo 替代
      wx.redirectTo({
        url: '/subpkg-order/pages/detail/detail?id=123'
      })
    } else {
      wx.navigateTo({
        url: '/subpkg-order/pages/detail/detail?id=123'
      })
    }
  }
})

8.4 分包中的 tabBar 配置

tabBar 页面必须在主包中,不能放在分包里:

json复制

复制代码
// ❌ 错误:tabBar 页面在分包中
{
  "pages": ["pages/index/index"],
  "subpackages": [
    { "root": "subpkg", "pages": ["pages/home/home"] }
  ],
  "tabBar": {
    "list": [
      { "pagePath": "pages/index/index", "text": "首页" },
      { "pagePath": "subpkg/pages/home/home", "text": "主页" }
    ]
  }
}
// tabBar 中的 pagePath 不能是分包页面

// ✅ 正确:tabBar 页面都在主包
{
  "pages": [
    "pages/index/index",
    "pages/home/home",
    "pages/cart/cart",
    "pages/user/user"
  ],
  "tabBar": {
    "list": [
      { "pagePath": "pages/index/index", "text": "首页" },
      { "pagePath": "pages/home/home", "text": "主页" },
      { "pagePath": "pages/cart/cart", "text": "购物车" },
      { "pagePath": "pages/user/user", "text": "我的" }
    ]
  }
}

8.5 分包资源引用路径

javascript复制

复制代码
// 分包中的图片引用
// ❌ 使用相对路径引用主包资源
// 在分包 subpkg-order 中的 wxml:
<image src="../../images/icon.png" />  // 可能找不到

// ✅ 使用绝对路径
<image src="/images/icon.png" />

// ✅ 使用 CDN 地址
<image src="{{cdnUrl}}/icon.png" />

总结

分包加载与体积控制是一个需要从架构设计阶段就开始考虑的问题。以下是一份完整的检查清单:

code复制

复制代码
✅ 体积检查清单
├── 主包 ≤ 2MB
│   ├── 高频页面在主包
│   ├── tabBar 页面在主包
│   └── 公共组件/工具在主包
├── 总包 ≤ 20MB
│   ├── 分包按功能/场景/频率划分
│   ├── 低频页面放分包
│   └── 大资源走 CDN
├── 预加载策略
│   ├── 首页预加载核心分包
│   ├── 高频路径配置 preloadRule
│   └── 单页预下载 ≤ 2 个分包
├── 资源优化
│   ├── 图片 → WebP + tinypng
│   ├── 字体 → 子集化
│   ├── npm → 按需引入
│   └── 大文件 → CDN
└── 避坑检查
    ├── 分包 root 不与主包路径冲突
    ├── navigateTo 层级 < 10
    ├── tabBar 页面在主包
    └── 资源引用使用绝对路径

最后强调一点:体积优化不是一劳永逸的。随着业务迭代,代码和资源会不断膨胀。建议在 CI/CD 流程中加入体积检查,每次提交自动检测包大小,超限时阻止合并:

yaml复制

复制代码
# .gitlab-ci.yml / .github/workflows/size-check.yml
size-check:
  stage: test
  script:
    - node scripts/analyze-size.js
    - |
      MAIN_SIZE=$(node -e "const f=require('fs');const s=require('./size-report.json');console.log(s.main)")
      if [ $(echo "$MAIN_SIZE > 2097152" | bc) -eq 1 ]; then
        echo "❌ 主包超限: ${MAIN_SIZE} bytes > 2MB"
        exit 1
      fi
      echo "✅ 体积检查通过"

把体积控制纳入工程化流程,才能确保小程序在长期迭代中始终保持健康的体积。