最近做基于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
包构成,需要做一些处理:
- 提取大文件,如xlsx、antv、mapbox-gl、lodash等
- 按需加载等
具体优化方案
提升构建速度
- 开发配置
- 环境变量启用
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: {},
}
处理包大小
- 压缩代码,移除注释/日志,配置
terserOptions
或transform-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问题
解决方案:
- 更换备用CDN地址,如
cdn.jsdelivr.net
->fastly.jsdelivr.net
或gcore.jsdelivr.net
,后两者可能 - 也可以发布到自己的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的选择
splitChunks
和dynamicImport
都一定程度能提升打包速度,但前者有些坑
- 如果没有拆出指定的包(如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,
},
},
},
}
});
},
}
小结
本文权当经验记录,也有不成熟的地方,可以评论交流~