下面是我对一个庞大的多页面项目优化的总结,有些评论仅代表我在优化过程遇到的。优化方法、用法我都列举了,望君自行斟酌取舍
一、分析工具
- 1、speed-measure-webpack-plugin
// webpack.dev.conf.js / webpack.prod.conf.js
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();
module.exports = smp.wrap(YourWebpackConfig);
speed-measure-webpack-plugin
,它能够测量出在你的构建过程中,每一个Loader
和Plugin
的执行时长- tips:如果你有自定义
Plugin
,有用到html-webpack-plugin
提供的hooks
,请先移除,否则会报错
- 2、cpuprofile-webpack-plugin
// webpack.base.conf.js
const CpuProfilerWebpackPlugin = require('cpuprofile-webpack-plugin');
module.exports = {
plugins: [
new CpuProfilerWebpackPlugin()
]
}
- 打包后会在你的项目下生成
profile
文件夹,文件夹里生成的分析的html
文件,用浏览器打开就可以了
二、优化途径:缓存、多核、拆分、抽离
打包慢发现主要因为这两个阶段:
- 1、
babel
等loaders
解析阶段 - 2、
js
、css
压缩阶段
(一)缓存
tips:存在更新依赖后依旧命中缓存的
bug
,开发机上删除node_modules/.cache
解决,但是如果集成在自动化CI
流程就麻烦点,除非依赖不更新,否则不建议在CI
流程使用
- 1、vue-loader缓存
// webpack.base.conf.js
{
test: /\.vue$/,
loader: 'vue-loader',
options: {
+ cacheDirectory: './node_modules/.cache/vue-loader',
+ cacheIdentifier: 'vue-loader',
}
},
- 2、babel-loader缓存
// webpack.base.conf.js
{
test: /\.js$/,
loader: 'babel-loader',
+ options: {
+ cacheDirectory: true,
+ },
exclude: [path.resolve(__dirname, '../node_modules')]
},
- 3、uglifyjs-webpack-plugin缓存
// webpack.prod.conf.js
new UglifyJsPlugin({
uglifyOptions: {
warnings: false,
compress: {
drop_console: true
},
},
sourceMap: false,
+ cache: true
}),
- 4、通过cache-loader
// webpack.base.conf.js
{
test: /\.js$/,
- loader: 'babel-loader',
+ use: ['cache-loader', 'babel-loader'],
include: path.resolve('src'),
},
// webpack.base.conf.js
{
test: /\.(less|css)$/,
use: [
_mode === 'development' ? 'style-loader' : MiniCssExtractPlugin.loader,
+ 'cache-loader', // 受MiniCssExtractPlugin实现的影响,放在MiniCssExtractPlugin之后才能生效
{
loader: 'css-loader',
options: {
importLoaders: 1,
import: true,
},
},
'postcss-loader',
],
},
(二)多核
多核虽好,请勿迷恋,过多反而拉慢速度。
- 1、uglifyjs-webpack-plugin多核运行
// webpack.prod.conf.js
new UglifyJsPlugin({
uglifyOptions: {
warnings: false,
compress: {
drop_console: process.env.WEHOTEL_ENV !== 'test'
},
},
sourceMap: false,
extractComments: false,
cache: true,
+ parallel: true,
}),
- 2、通过happypack
// webpack.base.conf.js
const HappyPack = require('happypack');
......
{
test: /\.js$/,
- use: ['cache-loader', 'babel-loader'], // 移到下面的loaders
+ loader: 'happypack/loader?id=happy-babel', // 这里的id和下面plugin的id保持一致
include: [resolve('src'), resolve('test')],
exclude: [path.resolve(__dirname, '../node_modules')]
},
......
plugins: [
+ new HappyPack({
+ id: 'happy-babel', // 这里的id和上面loader的id保持一致
+ loaders: ['cache-loader', 'babel-loader'], // 来自上面rule的use
+ threadPool: HappyPack.ThreadPool({ size: require('os').cpus().length }), // 设置核数量
+ verbose: false, // 是否打印信息
+ }),
......
// webpack.base.conf.js
// 测试下来不理想,我本人没有采用,仅供参考
{
test: /\.(less|css)$/,
use: [
_mode === 'development' ? 'style-loader' : MiniCssExtractPlugin.loader,
'cache-loader', // 这里因为MiniCssExtractPlugin的影响,放在MiniCssExtractPlugin之后才能生效
- {
- loader: 'css-loader',
- options: {
- importLoaders: 1,
- import: true,
- },
- },
+ 'happypack/loader?id=happy-css',
- 'postcss-loader',
+ 'happypack/loader?id=happy-postcss',
],
},
......
plugins: [
+ new HappyPack({
+ id: 'happy-css', // 这里的id和上面loader的id保持一致
+ loaders: [
+ {
+ loader: 'css-loader',
+ options: {
+ importLoaders: 1,
+ import: true,
+ },
+ }
+ ], // 来自上面的loader
+ threadPool: HappyPack.ThreadPool({ size: require('os').cpus().length }), // 设置核数量
+ verbose: false, // 是否打印信息
+ }),
+ new HappyPack({
+ id: 'happy-postcss', // 这里的id和上面loader的id保持一致
+ loaders: ['postcss-loader'], // 来自上面的loader
+ threadPool: HappyPack.ThreadPool({ size: require('os').cpus().length }), // 设置核数量
+ verbose: false, // 是否打印信息
+ }),
......
- 通过
happypack
,为loader
提供多个进程执行,明显加速,但是注意happypack
的数量,过多反而变慢。happypack支持的loader列表
- 3、通过thread-loader
官方推荐使用thread-loader
,但是测试下来,真的不行,thread-loader
自身每个worker
都需要花费时间,就算提前开启预热也没用,或者如同官方说的,请仅在耗时的loader
上使用
// webpack.base.conf.js
// 测试下来不理想,我本人没有采用,仅供参考
+ const threadLoader = require('thread-loader');
+ const jsWorkerPool = {
+ poolTimeout: 2000
+ };
+ const cssWorkerPool = {
+ workerParallelJobs: 2,
+ poolTimeout: 2000
+ };
+ threadLoader.warmup(jsWorkerPool, ['babel-loader']);
+ threadLoader.warmup(cssWorkerPool, ['css-loader', 'postcss-loader']);
{
test: /\.js$/,
exclude: /node_modules/,
use: [
+ 'thread-loader',
'babel-loader'
]
},
{
test: /\.s?css$/,
exclude: /node_modules/,
use: [
'style-loader',
+ 'thread-loader',
{
loader: 'css-loader',
options: {
modules: true,
localIdentName: '[name]__[local]--[hash:base64:5]',
importLoaders: 1
}
},
'postcss-loader'
]
}
(三)拆分
// webpack.prod.conf.js / webpack.dev.conf.js
+ optimization: {
+ moduleIds: 'hashed', // 有利于缓存
+ chunkIds: 'size', // 有利于缓存
+ mangleWasmImports: true, // 告知 webpack 通过将导入修改为更短的字符串
+ splitChunks: {
+ chunks: 'initial', // 用于命中chunk,function (module, chunk) | RegExp | string
+ cacheGroups: {
+ common: {
+ chunks: 'initial', // all、async、initial,默认async
+ minChunks: 2, // 最小共用模块数
+ name: 'common', // 模块名
+ priority: 9, // 优先级
+ enforce: true // 忽略splitChunks设置
+ },
+ vendor: {
+ test: /node_modules/, // 用于命中chunk,function (module, chunk) | RegExp | string
+ chunks: 'initial', // all、async、initial,默认async
+ name: 'vendor', // 模块名
+ priority: 10, // 优先级
+ enforce: true // 忽略splitChunks设置
+ }
+ }
+ },
+ runtimeChunk: {
+ name: 'manifest' // 将入口模块中的runtime部分提取出来
+ }
+ },
......
plugins: [
- ...... // 这里省略删除CommonsChunkPlugin代码
new HtmlWebpackPlugin({
title: 'title',
filename: 'index.html',
template: './src/index.html',
inject: true,
minify: {
removeComments: true,
collapseWhitespace: true,
removeAttributeQuotes: true
},
chunksSortMode: 'dependency',
+ chunks: ['manifest', 'vendor', 'common', name] // 单页面可以不用配置chunks
})
- 最难的是找到一个适当的拆分设置,上面的设置仅供参考
- 适当的拆分,可以优化整个打包文件的大小
- 适当的拆分,可以优化开发环境热更新的速度
webpack4
将CommonsChunkPlugin
废弃,由optimization.splitChunks
和optimization.runtimeChunk
替代,前者拆分代码,后者提取runtime
代码- 官方文档 优化(optimization)
- 官方文档 SplitChunksPlugin
(四)抽离
dll
抽离不建议使用:
1、要提前打包,再集成到webpack打包里面,不利于集成到自动化流程
2、依赖更新又要重新打包,有维护成本,忘记就GG
了
3、提前打包的js
要插入到html
中
4、测试下打包性能提升,效果不明显,在开发环境反而拉慢了速度
externals
抽离不建议使用:
1、要考虑各个引用的和项目使用的版本一致
2、升级依赖包,要及时把引用的版本也更新,有维护成本,忘记就GG
了
3、引用包过多,拉慢加载速度,除非有http2
的多路复用
4、引用的文件,如果用第三方会有cdn
不稳定,要自己部署cdn
5、不同的包之间可能有重复引用,增大总体积
6、就算你把所有的引用打包成一个文件,部署cdn
再引用,上面的问题也有的还是存在- 1、dll
// ddl.config.js
const webpack = require('webpack');
const vendors = [
'react',
'react-dom',
'react-router',
// ...其它库
];
module.exports = {
output: {
path: 'build',
filename: '[name].js',
library: '[name]',
},
entry: {
"lib": vendors,
},
plugins: [
new webpack.DllPlugin({
path: 'manifest.json', // manifest.json 文件的输出路径,这个文件会用于后续的业务代码打包
name: '[name]', // dll暴露的对象名,要跟 output.library 保持一致
context: __dirname, // 解析包路径的上下文,这个要跟接下来配置的 webpack.config.js 一致
}),
],
};
- 首先新增配置文件
ddl.config.js
// package.json
"scripts": {
+ "build:dll": "webpack --mode production --config ddl.config.js",
......
- 运行
npm run build:dll
,会输出两个文件:lib.js
、manifest.json
// webpack.prod.conf.js
plugins: [
+ new webpack.DllReferencePlugin({
+ context: __dirname, // 需要跟之前保持一致,这个用来指导 Webpack 匹配 manifest 中库的路径
+ manifest: require('./manifest.json'), // 用来引入刚才输出的 manifest.json 文件
+ }),
......
- 通过
webpack.DllReferencePlugin
引入dll
// webpack.prod.conf.js
+ var HtmlWebpackTagsPlugin = require('html-webpack-tags-plugin');
plugins: [
new webpack.DllPlugin({
path: 'manifest.json', // manifest.json 文件的输出路径,这个文件会用于后续的业务代码打包
name: '[name]', // dll暴露的对象名,要跟 output.library 保持一致
context: __dirname, // 解析包路径的上下文,这个要跟接下来配置的 webpack.config.js 一致
}),
+ new HtmlWebpackTagsPlugin({ tags: ['lib.js'], append: false})
- 通过
html-webpack-tags-plugin
往html
插入dll
打包出来的js
文件
- 2、externals
// webpack.base.conf.js
externals: {
// key是我们 import 的包名,value 是CDN为我们提供的全局变量名
// 所以最后 webpack 会把一个静态资源编译成:module.export.react = window.React
"react": "React",
"react-dom": "ReactDOM",
"redux": "Redux",
"react-router-dom": "ReactRouterDOM"
}
与此同时,我们需要在html
中插入script
标签
// index.html
+
+ ...... // 其他引用的script
三、其他优化
- 1、缩小编译范围
优化效果并不明显
// webpack.base.conf.js
+ const resolve = dir => path.join(__dirname, '..', dir);
// ...
+ resolve: {
+ modules: [ // 指定以下目录寻找第三方模块,避免webpack往父级目录递归搜索
+ resolve('src'),
+ resolve('node_modules'),
+ resolve(config.common.layoutPath)
+ ],
+ mainFields: ['main'], // 只采用main字段作为入口文件描述字段,减少搜索步骤
+ alias: {
+ vue$: "vue/dist/vue.common",
+ "@": resolve("src") // 缓存src目录为@符号,避免重复寻址
+ }
+ },
+ module: {
+ noParse: /jquery|lodash/, // 忽略未采用模块化的文件,因此jquery或lodash将不会被下面的loaders解析
+ // noParse: function(content) {
+ // return /jquery|lodash/.test(content)
+ // },
+ rules: [
+ {
+ test: /\.js$/,
+ include: [ // 表示只解析以下目录,减少loader处理范围
+ resolve("src"),
+ resolve(config.common.layoutPath)
+ ],
+ exclude: file => /test/.test(file), // 排除test目录文件
+ loader: "happypack/loader?id=happy-babel" // 后面会介绍
+ },
+ ]
+ }
- 减少不必要的编译,即
modules
、mainFields
、noParse
、includes
、exclude
、alias
都用起来
- 2、tree-shaking
tree shaking
设计的初衷应该是shaking
掉第三方引入的样式中无用的代码。业务代码,尤其像.vue
这样的组件化开发tree shaking
的使用有限。反正我打开后各种问题,就弃坑了
// package.json
......
sideEffects: false
......
- 设置
sideEffects: false
告诉编译器该项目或模块是pure
的(所有文件都没有副作用),可以进行无用模块删除
// package.json
"sideEffects": [
"*.css*",
"*.vue"
],
.css
文件、.vue
文件模块有副作用,需要在打包的时候不要错误删除了这些模块的代码
四、总结
打完收工。