记一次Taro 3.6 H5端构建包体积异常问题排查

0 阅读6分钟

概述

首先介绍一下我们公司的前端跨端框架技术栈:

  • React 18.x
  • Taro 3.x
  • TypeScript 5.x
  • pnpm 8.x

某个大型项目开发需求,项目目录结构类似这样:

├── .editorconfig
├── .eslintignore
├── .eslintrc
├── .gitignore
├── .npmrc
├── .prettierrc
├── README.md
├── components/
│   ├── backend-api/
│   ├── backend-pro/
│   ├── data/
│   ├── form-rules/
│   ├── react-hooks/
│   ├── request/
│   ├── taro-design/
│   ├── taro-extend-types/
│   ├── taro-utils/
│   └── utils/
├── package.json
├── packages/
│   ├── backend/
│   ├── pos/
│   ├── staff/
│   └── user/
├── patches/
│   └── jotai@2.6.0.patch
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── tsconfig.backend.json
├── tsconfig.json
└── tsconfig.taro.json

开发阶段都非常顺利。但是进入提测环境后,某天客户技术跟我说某个H5打包体积异常,并且给我发了这么一个截图:

sItu3lpDG7MD

我一看,好家伙!这个项目确实页面不少,但也不至于如此夸张。我只好回复一个「收到」并开始进行排查 🕵️‍♂️

排查过程

复现流程

首先拉取相关分支代码并进行本地打包,查看构建输出产物体积。输出结果发现 dist 确实有八十几兆。😱
那么开始排查打包 bundle,首先祭出 webpack-bundle-analyzer 分析神器。大概结果如下:

xL7ibku8tZ9h Ua8ea1EOLYpt

通过上面的图我们实际上已经可以发现问题了:

  • 每个 chunk 体积都有一点几兆
  • 查看前三个 chunk 发现每个 chunk 都引入了相同的几个包:taro-componentstaro-design

基于此,我们继续查看一下 Taro 关于 splitChunk 的配置规则

查看配置

config/index.js 中添加如下修改:

const config = {
  // 省略其他配置
  h5: {
    webpackChain(config, webpack) {
      fs.writeFileSync(path.resolve(__dirname, 'output.js'), config.toString())
    }
  }
}

再次运行后查看 output.js,可以发现相关配置如下:

{
  entry: {
    app: [
      'D:\\projects\\xx-taro\\packages\\staff\\src\\app.config'
    ]
  }
  optimization: {
    minimize: true,           // 启用代码压缩
    nodeEnv: 'production',    // 强制设置为生产环境(会启用生产优化)
    chunkIds: 'deterministic',// 生成确定性 Chunk ID(利于长效缓存)
    removeEmptyChunks: true   // 移除空 Chunk
    splitChunks: {
      chunks: 'initial',       // 仅拆分同步加载的代码(异步加载的代码不处理)
      hidePathInfo: true,      // 隐藏路径信息(避免泄露文件结构)
      minSize: 0,             // 允许拆分包的最小大小为 0(强制拆分小模块)
      cacheGroups: {          // 自定义拆包规则
        'default': false,      // 禁用默认拆包规则
        defaultVendors: false,// 禁用默认的 `node_modules` 拆包规则
        // 自定义公共模块组
        common: {
          name: false,         // 自动生成名称
          minChunks: 2,        // 被 2 个及以上 Chunk 引用的模块
          priority: 1          // 优先级较低
        },
        // 自定义第三方库组
        vendors: {
          name: false,
          minChunks: 2,        // 被 2 个及以上 Chunk 引用的第三方库
          test: (module) => /[\\/]node_modules[\\/]/.test(module.resource),
          priority: 10          // 优先级高于 common
        },
        // Taro 框架专用组
        taro: {
          name: false,
          test: (module) => /@tarojs[\\/][a-z]+/.test(module.context),
          priority: 100         // 最高优先级(优先单独打包)
        }
      }
    }
  }
}

仔细查看 Taro 的 H5 构建配置信息,可以发现几个明显的问题:

  • 拆包策略问题chunks: 'initial' 配合 minChunks: 2 策略,但是由于 entry 只有单入口,导致 common/vendors 拆包失效
  • 动态加载不匹配:H5 项目 subpackages 会处理为动态加载,不适配 initial 的 chunk 机制,导致 taro 组 chunk 也失效
// app.config.ts
// 动态加载的子包会生成独立 Chunk
export default {
  pages: [
    'pages/index/index',
    'pages/detail/detail'
  ],
  // 动态加载子包
  subpackages: [
    {
      root: 'subpackage',
      pages: ['pageA', 'pageB']
    }
  ]
}

修改打包配置

综合分析:Taro H5 的构建配置是针对多入口的 chunk 规则,并不适合当前项目的构建规则。所以基于项目实际情况,我们将 chunk 规则修改如下:

{
  optimization: {
    splitChunks: {
      // 修改为 'all' 以包含同步和异步模块
      chunks: 'all',
    // 设置合理的最小分割大小,避免生成过多小文件 (单位:字节)
    minSize: 20000, // 示例值,可根据实际情况调整
    // maxInitialRequests 和 maxAsyncRequests 也可以考虑设置,避免单个入口请求太多 chunk
    // maxInitialRequests: 10, // 示例值
    // maxAsyncRequests: 10,   // 示例值
    cacheGroups: {
      default: false,
      defaultVendors: false,
      common: {
        name: 'chunk-common',
        minChunks: 2,
        priority: 1,
        reuseExistingChunk: true // 保持良好实践
      },
      vendors: {
        name: 'chunk-vendors',
        // minChunks: 2 在 chunks: 'all' + minSize 下也可以,取决于你期望的行为
        // 1 也可以考虑,只要 minSize 够大,就不会分割很小的单用库
        minChunks: 2,
        test: (module) =>
          /[\\/]node_modules[\\/]/.test(module.resource) &&
          !/[\\/]taro-design[\\/]/.test(module.resource) &&
          !/[\\/](react|react-dom|dayjs)[\\/]/.test(module.resource), // 排除已被更高优先级组捕获的模块
        priority: 10,
        reuseExistingChunk: true
      },
      react: {
        name: 'chunk-react',
        test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
        priority: 102,
        reuseExistingChunk: true
      },
      dayjs: {
        name: 'chunk-dayjs',
        test: /[\\/]node_modules[\\/](dayjs)[\\/]/,
        priority: 102,
        reuseExistingChunk: true
      },
      taro: {
        name: 'chunk-taro',
        reuseExistingChunk: true,
        test: (module) => /@tarojs[\\/]/.test(module.context),
        priority: 100
      },
      taroDesign: {
        name: 'chunk-taro-design',
        test: (module) => /[\\/]taro-design[\\/]/.test(module.resource),
        priority: 101,
        reuseExistingChunk: true
      }
    }
  }
}

让我们重新运行一下构建命令看看效果:

p7sogTHAr4eH

文件只剩六点几兆了!优化率高达 92%!!!效果还是非常好的,可以美滋滋地交差啦 🎉

继续优化

经过上一次优化,我们的优化率已经很高了,但仍然还有优化空间。作为一个有追求的前端工程师,我们进一步排查代码,针对项目做了更加极致的优化。包括:

1. 异步加载城市数据

  const [remoteData, setRemoteData] = useState<ICityData[]>([])
  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await Taro.request({
          timeout: 5000, // Set a timeout for the request
          url: 'https://aliyuncs.com/district.json'
        })
        setRemoteData(response.data)
      } catch (error) {
        console.error('Failed to fetch district data:', error)
      }
    }
    if (!propData) {
      fetchData()
    }
  }, [propData])

2. 生产环境移除调试工具

{
  copy: {
    patterns: [
      { from: 'src/static/', to: outputRoot + '/static/', ignore: REACT_APP_ENV === 'prod' ? ['**/vconsole.min.js'] : [] },
    ]
  }
}

3. 防止大图片被转换为 base64

{
  h5: {
    imageUrlLoaderOption: {
      limit: false // 禁用图片转换为base64编码
    },
  }
}

4. 打包时不生成 LICENSE.txt 文件

{
  webpackChain(config, webpack) {
    // 不生成LICENSE.txt文件
    config.optimization.minimizer('terserPlugin').tap((args) => {
      args[0].extractComments = false
      return args
    })
  }
}

再次运行查看dist包体积

t9hDrk9O617w

上图是时隔半年后迭代多轮需求后打的包

经过这一系列优化后,最终打包输出体积从原来的 80+ MB 降低到了 4-5 MB,整体优化率达到了惊人的 94%!🚀

总结

这次 Taro H5 构建包体积优化之旅,让我们深刻体会到了性能优化的重要性和技巧性。通过这个案例,我们学到了:

核心收获

  1. 深入理解 webpack splitChunks 机制
    掌握了 chunks: 'initial' vs chunks: 'all' 的区别,以及如何根据项目特点选择合适的拆包策略

  2. Taro 构建配置的定制化
    学会了如何查看和修改 Taro 的构建配置,针对具体项目需求进行个性化优化

  3. 多维度优化思路
    从代码分割、资源加载、环境配置等多个角度进行优化,形成了完整的优化方案

优化策略总结

  • 主要优化:修改 splitChunks 配置(优化率 92%)
  • 细节优化:异步加载、移除调试工具、图片处理等(额外优化 2%)
  • 最终效果:总体积从 80+ MB 降至 4-5 MB,优化率 94%

经验启示

这个案例告诉我们,工具配置的默认值并不总是最优解。当项目规模和复杂度达到一定程度时,我们需要:

  • 🔍 主动分析:使用 webpack-bundle-analyzer 等工具深入分析
  • 🎯 精准定位:找到问题的根本原因,而不是表面现象
  • 系统优化:从配置、代码、资源等多个维度进行优化
  • 📊 量化效果:用数据说话,验证优化效果

记住:性能优化是一门艺术,需要理论与实践相结合,工具与经验并重! 💪

希望这篇文章能帮助到遇到类似问题的同学们。如果你也有类似的优化经验,欢迎分享交流!