uniapp-降低主包体积-分包js

网上大量篇幅在讲如何分包,降低图片资源等等策略,但针对一些陈年老项目大家会发现,这些策略都已做过,那么该如何降低包体积呢?

在 uni-app 小程序开发中,主包体积超限 是最常见的痛点之一。官方默认的split-chunks策略存在一个硬伤:一个 JS 模块只要被 2 个及以上分包同时引用,即便主包完全不用,也会被强制打包进主包common/vendor,直接导致主包体积暴涨,甚至无法发布。

为了解决这个问题,写一个插件,专门针对小程序平台,自动将多分包共享的模块从主包剥离,分别下沉到对应分包的 vendor 中,精准瘦身主包。

一、先搞懂:为什么主包会莫名其妙变大

uni-app 小程序默认分包优化规则:

  1. 仅被单个分包使用的模块 → 自动打包到该分包 vendor(最优)
  2. 主包 + 任意分包使用的模块 → 打包进主包 vendor(合理)
  3. 2 个及以上分包 使用、主包未使用的模块 → 强制打包进主包 vendor( 问题根源)

这就导致:很多公共工具类、组件、工具库,只要被多个分包共享,就会挤爆主包体积。

二、插件核心能力

SplitUtilPlugin 专门解决上述第 3 种场景:

  • 仅针对小程序平台生效,不影响 H5/APP
  • 自动识别:多分包共享、主包未引用的模块
  • 自动剥离:从主包common/vendor移除
  • 自动分发:分别打包到对应分包的common/vendor
  • 安全兜底:保留主包引用的模块,不破坏业务逻辑
  • 可视化日志:清晰查看模块移动记录,方便排查

三、完整插件代码

javascript 复制代码
/**
 * SplitUtilPlugin
 * 解决 uni-app 主包体积过大问题
 * 核心:将【多分包共享、主包未使用】的模块从主包vendor移至对应分包vendor
 */
'use strict'

const path = require('path')
const GraphHelpers = require('webpack/lib/GraphHelpers')

const PLUGIN_NAME = 'SplitUtilPlugin'
const MAIN_VENDOR = 'common/vendor'

// 路径标准化
function normalizePath(p) {
  return (p || '').replace(/\\/g, '/')
}

// 根据名称查找chunk
function findChunkByName(chunks, name) {
  return chunks.find((c) => c.name === name)
}

class SplitUtilPlugin {
  apply(compiler) {
    // 仅小程序平台生效
    const platform = process.env.UNI_PLATFORM || process.env.VUE_APP_PLATFORM
    const isMP = platform && platform.startsWith('mp-')
    if (!isMP) {
      console.log(`[${PLUGIN_NAME}] 非小程序平台,插件跳过。`)
      return
    }

    compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
      // webpack优化阶段:splitChunks已完成,chunk归属已确定
      compilation.hooks.optimizeChunkModules.tap(
        PLUGIN_NAME,
        (chunks, modules) => {
          // 获取所有分包根目录
          const subPackageRoots = Object.keys(
            process.UNI_SUBPACKAGES || {},
          ).map((root) => normalizePath(root) + '/')

          if (subPackageRoots.length === 0) {
            console.warn(`[${PLUGIN_NAME}] 未配置分包,跳过。`)
            return
          }

          const chunkArr = Array.from(chunks)
          // 查找主包vendor
          const mainVendorChunk = findChunkByName(chunkArr, MAIN_VENDOR)
          if (!mainVendorChunk) {
            console.log(`[${PLUGIN_NAME}] 未找到主包vendor,跳过。`)
            return
          }

          // 判断chunk所属分包
          const getSubPackageRoot = (chunk) => {
            const name = normalizePath(chunk.name || '')
            return subPackageRoots.find((root) => name.startsWith(root)) || null
          }

          // 判断是否为主包chunk
          const isMainPackageChunk = (chunk) => !getSubPackageRoot(chunk)

          // 统计数据
          let movedCount = 0
          let skippedMainRef = 0
          let skippedSinglePkg = 0

          // 遍历所有模块
          for (const mod of modules) {
            // 仅处理真实JS文件,跳过CSS/虚拟模块
            if (!mod.resource || mod.type === 'css/mini-extract') continue

            const modChunks = mod.getChunks()
            // 仅处理当前只在主包vendor中的模块
            if (modChunks.length !== 1 || modChunks[0].name !== MAIN_VENDOR)
              continue

            // 收集所有引用该模块的业务chunk
            const issuerChunks = new Set()
            if (mod.reasons) {
              for (const reason of mod.reasons) {
                if (reason.module) {
                  for (const rc of reason.module.getChunks()) {
                    issuerChunks.add(rc)
                  }
                }
              }
            }

            // 兜底:获取真实引用chunk
            const referenceChunks = issuerChunks.size > 0
              ? Array.from(issuerChunks)
              : chunkArr.filter(c => c.name !== MAIN_VENDOR && c.hasModule?.(mod))

            // 安全规则:主包直接引用的模块,不移动
            const hasMainRef = referenceChunks.some(isMainPackageChunk)
            if (hasMainRef) {
              skippedMainRef++
              continue
            }

            // 收集所有引用该模块的分包
            const targetPkgRoots = new Set()
            for (const rc of referenceChunks) {
              const root = getSubPackageRoot(rc)
              if (root) targetPkgRoots.add(root)
            }

            // 仅处理多分包共享模块(单分包uni已自动优化)
            if (targetPkgRoots.size < 2) {
              skippedSinglePkg++
              continue
            }

            // 为每个目标分包创建/关联vendor chunk
            for (const pkgRoot of targetPkgRoots) {
              const targetChunkName = normalizePath(
                path.join(pkgRoot.replace(/\/$/, ''), MAIN_VENDOR),
              )
              let pkgChunk = findChunkByName(chunkArr, targetChunkName)
              
              // 不存在则新建分包vendor
              if (!pkgChunk) {
                const group = compilation.addChunkInGroup(targetChunkName)
                pkgChunk = group.chunks[0]
                chunkArr.push(pkgChunk)
              }
              
              // 连接模块与分包vendor
              GraphHelpers.connectChunkAndModule(pkgChunk, mod)
            }

            // 从主包vendor移除该模块
            GraphHelpers.disconnectChunkAndModule(mainVendorChunk, mod)
            movedCount++

            // 打印移动日志
            console.log(`[${PLUGIN_NAME}] 移动[${movedCount}]: ${normalizePath(mod.resource)} -> 分包: ${Array.from(targetPkgRoots).join(', ')}`)
          }

          // 最终统计
          console.log(`[${PLUGIN_NAME}] 处理完成:移动=${movedCount} | 跳过(主包引用)=${skippedMainRef} | 跳过(单分包)=${skippedSinglePkg}`)
        },
      )
    })
  }
}

module.exports = SplitUtilPlugin

四、vue.config.js 配置教程

  1. 将插件文件放在项目根目录src目录下
  2. 打开项目根目录的vue.config.js,添加配置:
javascript 复制代码
const SplitUtilPlugin = require('./split-util-plugin')

module.exports = {
  configureWebpack: {
    plugins: [
      // 注入分包优化插件
      new SplitUtilPlugin()
    ]
  }
}
相关推荐
西洼工作室13 小时前
UniApp开发全攻略:从生命周期到路由传值
前端·javascript·uni-app
Martin -Tang2 天前
uniapp+vue3+ts自定义表格
javascript·vue.js·uni-app
LinMin_Rik2 天前
解决win11专业版HbuilderX编译vue3的uniappX失败问题
uni-app
游戏开发爱好者82 天前
iOS应用性能监控:Pre-Main与Main函数耗时分析及Time Profiler使用教程
android·ios·小程序·https·uni-app·iphone·webview
天渺工作室3 天前
构建完自动打包压缩,一个插件搞定 Vite / Webpack / Rollup / ESBuild
webpack·vite·rollup.js
西洼工作室3 天前
个人开发者接入阿里云号码认证服务AliCloud-NirvanaPns实现一键登录
python·阿里云·uni-app·全栈·认证授权
2501_916008893 天前
ChatGPT前端开发学习指南:Visual Studio Code与谷歌浏览器安装配置详解
ide·vscode·ios·小程序·uni-app·编辑器·iphone
Hello--_--World4 天前
vite:什么是热更新?vite 和 webpack 有什么区别?vite常见配置和优化手段?
前端·webpack·node.js
Hello--_--World4 天前
Vite:什么是bundleless?哪些要打包,哪些不要打包?依赖预构建是什么?依赖预构建如何减少网络请求的?esbuild 又是什么?
前端·javascript·webpack·vite
Rooting++4 天前
vue2+webpack打包优化的相关问题
前端·webpack·node.js