umi3项目打包优化

最近做基于umi3的中后台项目,饱受构建耗时影响,启动构建7分钟,热更新半分钟,严重影响开发和调试效率,遂决定优化一波,以本文记录经验分享大家。

打包优化目标和基本思路

  • 构建耗时优化
    • 耗时分析
    • 优化方案:umi配置优化;忽略nodeModules处理;启用webpack5;舍弃用不到的多语言locale文件;Monaco配置优化等
    • 开发环境单独处理:关闭压缩、减少兼容浏览器版本、启用esbuild
    • 优化结果:构建耗时从7min降到2min(-71%),热更新从30s到10s(-66%)
  • 构建包体积优化
    • 包大小分析
    • 优化:提取大文件,设置externals;设置splitChunks;dynamicImport按需加载
    • 处理结果:首屏包大小从35M降到13M(-62%)

接下来将以这种思路,对项目的优化具体措施做分享。

项目版本:umi 3.5+,react 17+,webpack 5

打包耗时和体积分析

构建耗时分析

一般webpack项目构建结束会有Compiled successfully in 耗时提示,如果想看具体在哪些环节耗时长,可以在script设置SPEED_MEASURE=CONSOLE(UMI文档)。对应webpack配置speed-measure-webpack-plugin插件。

包体积分析

  • 包体积分析:配置环境变量ANALYZE=1分析构成,也可以通过umianalyze配置定制分析报告,生成构建结果体积分析文件。对应webpack-bundle-analyzer插件,建议线上环境关闭。
javascript 复制代码
{
  // umi文档参考:https://v3.umijs.org/config#analyze
  // 配置具体含义见:https://github.com/umijs/umi-webpack-bundle-analyzer#options-for-plugin
  analyze: {
    analyzerMode: 'server',
    analyzerPort: 8888,
    openAnalyzer: true,
    // generate stats file while ANALYZE_DUMP exist
    generateStatsFile: false,
    statsFilename: 'stats.json',
    logLevel: 'info',
    defaultSizes: 'parsed', // stat  // gzip
  }
}

配置后,:8888端口会展示构建结果体积分布报告。这里能看出umi.js包构成,需要做一些处理:

  1. 提取大文件,如xlsx、antv、mapbox-gl、lodash等
  2. 按需加载等

具体优化方案

提升构建速度

  • 开发配置
    • 环境变量启用ANALYZE=1分析包构成,analyze配置可选
    • 不处理node_modules nodeModulesTransform: {type: 'none', exclude: []}
      • ⚠️注:如果出现报错chunk of umi not found,说明存在依赖包不能直接使用的情况,可以针对性设置exclude,或关闭这项配置
    • 启用esbuild,安装插件@umijs/plugin-esbuild
    • umi官网的一些推荐
      • 环境变量设置COMPRESS=none关闭压缩
      • 关闭sourceMap devtool: false
      • 减少要兼容的浏览器范围,设置target,默认umi配置为{ chrome: 49, firefox: 64, safari: 10, edge: 13, ios: 10 }
  • 其它配置
    • 启用webpack5
    • 设置webpack构建缓存cache
javascript 复制代码
// package.json script
// 环境变量设置:分析包构成、禁用压缩
 ANALYZE=1 COMPRESS=none umi dev

// 开发配置 config.dev.js
export default {

  // 关闭sourceMap
  devtool: false,

  // 关闭node_modules处理,提升构建速度。注:部分依赖不做处理会报错
  nodeModulesTransform: {
    type: 'none',
    exclude: [],
  },
  // 减少要兼容的浏览器范围,默认umi配置为{ chrome: 49, firefox: 64, safari: 10, edge: 13, ios: 10 }
  targets: {
    chrome: 79,
    firefox: false,
    safari: false,
    edge: false,
    ios: false,
  },
  
  // 启用 esbuild,安装插件 @umijs/plugin-esbuild
  esbuild: {},
  
  // 设置构建缓存
  chainWebpack: function (config, { webpack }) {
    config.merge({
      cache: {
        //  memory | filesystem,生产环境禁用缓存到memory
        type: 'filesystem',
      },
    });
  },
}

// config.js
export default {
  // 利用webpack5提升打包速度
  webpack5: {},
}

处理包大小

  • 压缩代码,移除注释/日志,配置terserOptionstransform-remove-console
  • 提取大文件,设置externals、scripts
  • 按需加载 dynamicImport或拆包splitChunks
  • ignoreMomentLocale、locale、monaco配置,移除未用到的多语言配置

案例如下:

javascript 复制代码
// config.js
export default {
  // 压缩代码
  terserOptions: {
    output: { comments: true }, // 移除注释
    compress: {
      drop_console: true, // 移除日志
    },
  },
  
  // 减少支持的多语言
  ignoreMomentLocale: true,
  locale: {
    baseNavigator: false,
    default: 'zh-CN',
    antd: false,
    title: false,
  },

  // 启用按需加载,拆分构建产物,需要的时候再下载JS。详见 https://v3.umijs.org/config#dynamicimport 
  dynamicImport: {
    loading: '@/pages/page-loading',
  },
  // 提取大文件
  externals: {
    'react': 'window.React',
    'react-dom': 'window.ReactDOM',
    'xlsx': 'XLSX',
  },
  scripts: [
    'https://unpkg.com/xlsx/xlsx.js',
    // 注:开发环境React建议用development版本,避免未指定key发生的错误
    ...process.env.NODE_ENV ==='development'? [
        'https://gw.alipayobjects.com/os/lib/react/16.13.1/umd/react.development.js',
        'https://gw.alipayobjects.com/os/lib/react-dom/16.13.1/umd/react-dom.development.js',
      ] : [
        'https://gw.alipayobjects.com/os/lib/react/16.13.1/umd/react.production.min.js',
        'https://gw.alipayobjects.com/os/lib/react-dom/16.13.1/umd/react-dom.production.min.js',
      ]
  ],
  
  // 删除console减少体积,依赖babel-plugin-transform-remove-console插件
  // extraBabelPlugins: [!isDev ? 'transform-remove-console' : ''],

  chainWebpack: (config) => {
    // 缩小monaco支持的语言
    config.plugin('monaco-editor-webpack-plugin').use(MonacoWebpackPlugin, [
      { languages: ['javascript'] }
    ]);
  },
}

踩到的坑

设置externals加载CDN资源502问题

解决方案:

  1. 更换备用CDN地址,如cdn.jsdelivr.net->fastly.jsdelivr.netgcore.jsdelivr.net,后两者可能
  2. 也可以发布到自己的CDN,如配置copy将特定目录资源移动到构建结果目录,一起上传到CDN
css 复制代码
{
  copy: [
    {
      from: 'assets',
      to: 'assets',
    },
  ],
}

其它尝试

  • 引用Happypack并行打包
  • 设置terser多进程处理
  • splitChunks/dynamicImport的选择

Happypack并行打包(舍弃)

构建耗时反而增加2分钟

javascript 复制代码
// 需要安装thread-loader依赖
const HappyPack = require('happypack')
const os = require('os')

const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });
// 开启多进程打包
config.plugin('happypack').use(HappyPack, [
{
  id: 'js',
  // 根据 CPU 核心数设置线程池大小
  threads: os.cpus().length,
  // 需要并行处理的 loader
  loaders: ['babel-loader'],
},
]);

TerserPlugin/terserOptions(取需)

  • 压缩代码,减少包大小
  • 增加构建耗时
javascript 复制代码
// umi配置
{
    terserOptions: {
      output: { comments: true }, // 移除注释
      compress: {
        inline: false,
        drop_console: false, // 移除日志
      },
      mangle: {
        safari10: true,
      },
    }
}

// 或webpack配置
import TerserPlugin from 'terser-webpack-plugin';
import os from 'os';

config.plugin('TerserPlugin').use(TerserPlugin, [
  {
    parallel: os.cpus().length - 1,
    // https://github.com/terser/terser#minify-options
    terserOptions: {}, // 如上,省略
  },
]);

esbuild/webpack5的选择

  • 在本项目中,发现启用esbuild构建速度比启用webpack5
  • esbuild生产执行报错:Transforming for-await loops to the configured target environment is not supported yet
  • 因此开发环境用esbuild,生产环境启用webpack5

MSFU(暂舍)

构建速度提升不大

splitChunks/dynamicImport的选择

splitChunksdynamicImport都一定程度能提升打包速度,但前者有些坑

  • 如果没有拆出指定的包(如vendors),而chunks引用了vendors资源,就会报错vendors不存在,不够灵活
  • 因此选择dynamicImport
javascript 复制代码
{
  // 配置案例参考umi官方文档
  chunks: ['vendors', 'umi'],
  chainWebpack: function (config, { webpack }) {
    config.merge({
      optimization: {
        splitChunks: {
          chunks: 'all',
          minSize: 30000,
          minChunks: 3,
          automaticNameDelimiter: '.',
          cacheGroups: {
            vendor: {
              name: 'vendors',
              test({ resource }) {
                return /[\\/]node_modules[\\/]/.test(resource);
              },
              priority: 10,
            },
          },
        },
      }
    });
  },
}

小结

本文权当经验记录,也有不成熟的地方,可以评论交流~

参考资料

相关推荐
_.Switch39 分钟前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j
一路向前的月光43 分钟前
Vue2中的监听和计算属性的区别
前端·javascript·vue.js
长路 ㅤ   44 分钟前
vite学习教程06、vite.config.js配置
前端·vite配置·端口设置·本地开发
长路 ㅤ   44 分钟前
vue-live2d看板娘集成方案设计使用教程
前端·javascript·vue.js·live2d
Fan_web1 小时前
jQuery——事件委托
开发语言·前端·javascript·css·jquery
安冬的码畜日常1 小时前
【CSS in Depth 2 精译_044】第七章 响应式设计概述
前端·css·css3·html5·响应式设计·响应式
莹雨潇潇2 小时前
Docker 快速入门(Ubuntu版)
java·前端·docker·容器
Jiaberrr2 小时前
Element UI教程:如何将Radio单选框的圆框改为方框
前端·javascript·vue.js·ui·elementui
Tiffany_Ho3 小时前
【TypeScript】知识点梳理(三)
前端·typescript
安冬的码畜日常4 小时前
【D3.js in Action 3 精译_029】3.5 给 D3 条形图加注图表标签(上)
开发语言·前端·javascript·信息可视化·数据可视化·d3.js