我们的后台系统是前端单页应用(vue + webpack2),经过多次迭代,目前打包进度上统计已有3000+ 模块,最近感觉项目的运行,打包时间越来越慢,刚好时间比较宽松,优化了一把在这里记录下。
优化项
1. 构建工具升级/替换 webpack2------webpack4 ✔
2. 缓存 ---- HardSourceWebpackPlugin ✔
3. 多进程 ---- thread-loader ✔
4. 寻路优化 ----- oneOf ✔
寻路优化; 缓存; 构建工具升级这些基本上可以无脑直接抄网上的作业。多进程需要分析一下项目的耗时步骤再用,因为开一个进程也需要时间,用不好反而更耗时
配置记录
考虑到webpack版本比较老,考虑先升级,经过调研,打算将webpack版本切换到4,由于版本相差较大,从0开始重写webpack配置,这部分其实没啥技术含量,最重要的是找到各插件的兼容版本;如npm webpack,weback和其配套插件之间,babel-loader之间的,vue版本vue-loader等各种loader中间的版本冲突问题,需要花费大量的精力去查,尝试。得出来webpack4可用的依赖包(package.json),webapck开发、生产环境(webpack.common.js,webpack.dev.js,webpack.prod.js)配置.
package.json依赖文件
"scripts": {
"dev": "cross-env NODE_ENV=development webpack-dev-server --config ./build/webpack.dev.js",
"build": "cross-env NODE_ENV=production webpack --config ./build/webpack.prod.js"
},
"dependencies": {
"@sentry/browser": "^5.24.2",
"@vant/touch-emulator": "^1.2.0",
"animate.css": "^3.7.2",
"async-validator": "^3.2.4",
"broadcast-channel": "^3.4.1",
"child_process": "^1.0.2",
"crypto-js": "^4.0.0",
"echarts": "^3.8.5",
"element-ui": "^2.15.2",
"file-saver": "^2.0.1",
"fs": "^0.0.1-security",
"html2canvas": "^1.0.0-rc.7",
"install": "^0.13.0",
"jquery": "^3.3.1",
"js-base64": "^2.1.9",
"js-md5": "^0.6.0",
"lodash": "^4.17.15",
"mint-ui": "^2.2.11",
"nprogress": "^0.2.0",
"qrcode.vue": "^1.6.3",
"qs": "^6.10.1",
"sha1": "^1.1.1",
"uglify-es": "^3.3.9",
"uuid": "^8.3.2",
"vant": "^2.12.3",
"vconsole": "^3.4.0",
"vue": "^2.6.10",
"vue-3d-picker": "^2.1.0",
"vue-awesome-swiper": "^3.1.3",
"vue-clipboard2": "0.0.8",
"vue-cropper": "^0.5.6",
"vue-lottie": "^0.2.1",
"vue-swiper-component": "^2.1.3",
"vuedraggable": "^2.23.2",
"webpackbar": "^5.0.2",
"weixin-js-sdk": "^1.6.0",
"xlsx": "^0.18.5"
},
"devDependencies": {
"autoprefixer": "^9.6.5",
"babel-core": "^6.22.1",
"babel-eslint": "^7.1.1",
"babel-loader": "^7.1.4",
"babel-plugin-dynamic-import-node": "^2.3.0",
"babel-plugin-import": "^1.13.0",
"babel-plugin-istanbul": "^4.1.1",
"babel-plugin-transform-runtime": "^6.22.0",
"babel-polyfill": "^6.26.0",
"babel-preset-env": "^1.3.2",
"babel-preset-stage-2": "^6.22.0",
"babel-register": "^6.22.0",
"clean-webpack-plugin": "^4.0.0",
"cross-env": "^7.0.3",
"css-loader": "^3.2.1",
"eslint": "^3.19.0",
"eslint-config-standard": "^6.2.1",
"eslint-friendly-formatter": "^2.0.7",
"eslint-loader": "^2.1.2",
"eslint-plugin-html": "^2.0.0",
"eslint-plugin-promise": "^3.4.0",
"eslint-plugin-standard": "^2.0.1",
"file-loader": "^4.2.0",
"hard-source-webpack-plugin": "^0.13.1",
"html-webpack-plugin": "^3.2.0",
"html-withimg-loader": "^0.1.16",
"less": "^3.10.3",
"less-loader": "^5.0.0",
"mini-css-extract-plugin": "^0.8.2",
"node-sass": "4.14",
"optimize-css-assets-webpack-plugin": "^5.0.8",
"postcss": "^8.4.21",
"postcss-loader": "^3.0.0",
"sass-loader": "^7.1.0",
"speed-measure-webpack-plugin": "^1.5.0",
"style-loader": "^1.0.2",
"terser-webpack-plugin": "^4.2.3",
"thread-loader": "^3.0.4",
"url-loader": "^2.2.0",
"vue-loader": "^15.7.0",
"vue-router": "^2.8.1",
"vue-style-loader": "^2.0.5",
"vue-template-compiler": "^2.6.10",
"webpack": "^4.41.6",
"webpack-cli": "^3.3.9",
"webpack-dev-server": "^3.8.2",
"webpack-merge": "^5.8.0"
},
"engines": {
"node": ">= 4.0.0",
"npm": ">= 3.0.0"
},
"browserslist": [
"defaults",
"not ie < 11",
"last 2 versions",
"> 1%",
"iOS 7",
"last 3 iOS versions"
]
webpack公共文件:webpack.common.js
const path = require('path')
const utils = require('./utils')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const {CleanWebpackPlugin} = require('clean-webpack-plugin');
const VueLoaderPlugin = require('vue-loader/lib/plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
// const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const WebpackBar = require('webpackbar');
const os = require('os');
function resolve (dir) {
return path.join(__dirname, '..', dir)
}
const commonConfig = {
entry: {
main: './src/main.js',
},
resolve: {
extensions: ['.js', '.vue', '.json', '.css', '.scss'],
alias: {
'vue$': 'vue/dist/vue.esm.js',
'@': resolve('src'),
'plugins': resolve('src/plugins'),
'assets': resolve('src/assets'),
'components': resolve('src/components'),
'utils': resolve('src/utils'),
'pages': resolve('src/pages'),
'swiper': 'swiper/dist/js/swiper.js'
}
},
module: {
rules: [
/*
'thread-loader'
开启多进程打包。
进程启动大概为600ms,进程通信也有开销。
只有工作消耗时间比较长,才需要多进程打包
*/
{
test: /\.(js|vue)$/,
loader: 'eslint-loader',
enforce: 'pre',
include: [resolve('src'), resolve('test')],
options: {
formatter: require('eslint-friendly-formatter')
}
},
{
test: /\.vue$/,
use: [
'thread-loader',
{
loader: 'vue-loader',
options: {
name: '[name].[hash:7].[ext]',
outputPath: utils.assetsPath('js/')
}
},
// 'thread-loader'
]
},
{
oneOf: [
{
test: /\.js$/,
exclude: /node_modules/,
use: [
'thread-loader',
{
loader: 'babel-loader',
},
]
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
use: {
loader: 'url-loader',
options: {
name: '[name].[hash:7].[ext]',
outputPath: utils.assetsPath('img/'),
limit: 10000 //100KB
}
}
},
{
test: /\.css$/,
use: [
// MiniCssExtractPlugin 推荐只用于生产环境,因为该插件在开发环境下会导致HMR功能缺失,所以日常开发中,还是用style-loader。
process.env.NODE_ENV === 'development' ? 'style-loader': MiniCssExtractPlugin.loader,
// 'thread-loader',
'css-loader'
]
},
{
test: /\.scss$/,
use: [
process.env.NODE_ENV === 'development' ? 'style-loader': MiniCssExtractPlugin.loader,
'thread-loader',
'css-loader',
'sass-loader'
]
},
{
test: /\.less$/,
use: [
process.env.NODE_ENV === 'development' ? 'style-loader': MiniCssExtractPlugin.loader,
'css-loader',
'less-loader'
]
},
{
test: /\.(woff2?|eot|ttf|otf|woff|woff2)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 100000,
name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
}
}
]
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './index.html' // 以src/目录下的index.html为模板打包
}),
new CleanWebpackPlugin({
// 不需要做任何的配置
}),
new VueLoaderPlugin(),
new WebpackBar(),
// new SpeedMeasurePlugin(),
],
externals: {
// 不打包进vendor,都用cdn加载
'vue': 'Vue',
'vue-router': 'VueRouter',
'vuex': 'Vuex',
'axios': 'axios',
'ali-oss': 'Oss'
},
output: {
filename: '[name].js',
// publicPath: "https://cdn.example.com/assets/",
path: path.join(__dirname, './dist')
}
}
module.exports = commonConfig
webpack dev文件:webpack.dev.js
const path = require('path')
const webpack = require('webpack')
const {merge} = require('webpack-merge')
const commonConfig = require('./webpack.common')
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin')
const devConfig = {
mode: 'development',
devtool: 'eval-cheap-module-source-map',
devServer: {
contentBase: path.join(__dirname, "dist"),
compress: true,
port: 9000,
open: true,
hot: true,
// hotOnly: true,
},
plugins: [
new webpack.NamedModulesPlugin(),
new webpack.HotModuleReplacementPlugin(),
new HardSourceWebpackPlugin({
// cacheDirectory是在高速缓存写入。默认情况下,将缓存存储在node_modules下的目录中
// 'node_modules/.cache/hard-source/[confighash]'
cacheDirectory: path.join(__dirname, '../disk-dev/.cache/hard-source/[confighash]'),
// configHash在启动webpack实例时转换webpack配置,
// 并用于cacheDirectory为不同的webpack配置构建不同的缓存
configHash: function(webpackConfig) {
// node-object-hash on npm can be used to build this.
return require('node-object-hash')({sort: false}).hash(webpackConfig);
},
// 当加载器、插件、其他构建时脚本或其他动态依赖项发生更改时,
// hard-source需要替换缓存以确保输出正确。
// environmentHash被用来确定这一点。如果散列与先前的构建不同,则将使用新的缓存
environmentHash: {
root: process.cwd(),
directories: [],
files: ['./../package-lock.json', './../package.json'],
},
// An object. 控制来源
info: {
// 'none' or 'test'.
mode: 'none',
// 'debug', 'log', 'info', 'warn', or 'error'.
level: 'debug',
},
// Clean up large, old caches automatically.
cachePrune: {
// Caches younger than `maxAge` are not considered for deletion. They must
// be at least this (default: 2 days) old in milliseconds.
maxAge: 2 * 24 * 60 * 60 * 1000,
// All caches together must be larger than `sizeThreshold` before any
// caches will be deleted. Together they must be at least this
// (default: 50 MB) big in bytes.
sizeThreshold: 100 * 1024 * 1024
},
})
],
optimization:{
usedExports: true
}
}
module.exports = merge(commonConfig, devConfig)
webpack prod文件:webpack.prod.js
const {merge} = require('webpack-merge')
const commomConfig = require('./webpack.common')
const path = require('path')
const utils = require('./utils')
// css抽离
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
// css压缩
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
// 去除无用代码
const TerserPlugin = require('terser-webpack-plugin')
// 打包缓存
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin')
const prodConfig = {
mode: 'production',
devtool: 'false',
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
styles: {
name: 'styles',
test: /\.css$/,
enforce: true, // 设置为true表示忽略 splitChunks.minSize、splitChunks.minChunks、splitChunks.maxAsyncRequests和splitChunks.maxInitialRequests的配置,为当前缓存组生成chunks。
},
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10, // 打包到vendorsChunk的优先级
},
default: {
minChunks: 2, // 表示 split 前单个非按需导入的 module 的并行数的最低下限,即某个模块的引用次数必须大于等于设置的数值,该模块才能被拆分出来;
priority: -20,
reuseExistingChunk: true // reuseExistingChunk是如果当前cacheGroup中如果存在已经被分割的库那么就会复用之前的,就比如业务代码中的lodash,业务很多个业务文件会import _ from lodash,这里只会打包一个
}
}
},
minimizer: [
new TerserPlugin({
parallel: true, // 使用多进程并发运行以提高构建速度 Boolean|Number 默认值: true
terserOptions: {
compress: {
drop_console: true,//移除所有console相关代码;
drop_debugger: true,//移除自动断点功能;
pure_funcs: ["console.log", "console.error"],//配置移除指定的指令,如console.log,alert等
},
format: {
comments: false,//删除注释
},
},
extractComments: false,//是否将注释剥离到单独的文件中
}),
new OptimizeCSSAssetsPlugin({})
]
},
plugins: [
new MiniCssExtractPlugin({
filename: utils.assetsPath(`css/[name].[contenthash].css`)
}),
new OptimizeCSSAssetsPlugin ({
// 默认是全部的CSS都压缩,该字段可以指定某些要处理的文件
assetNameRegExp: /\.(le|sa|sc|c)ss$/g,
// 指定一个优化css的处理器,默认cssnano
cssProcessor: require('cssnano'),
cssProcessorPluginOptions: {
preset: [ 'default', {
discardComments: { removeAll: true }, //对注释的处理
normalizeUnicode: false // 建议false,否则在使用unicode-range的时候会产生乱码
}]
},
// canPrint: true // 是否打印编译过程中的日志
}),
new HardSourceWebpackPlugin({
// cacheDirectory是在高速缓存写入。默认情况下,将缓存存储在node_modules下的目录中
// 'node_modules/.cache/hard-source/[confighash]'
cacheDirectory: path.join(__dirname, '../disk-prod/.cache/hard-source/[confighash]'),
// configHash在启动webpack实例时转换webpack配置,
// 并用于cacheDirectory为不同的webpack配置构建不同的缓存
configHash: function(webpackConfig) {
// node-object-hash on npm can be used to build this.
return require('node-object-hash')({sort: false}).hash(webpackConfig);
},
// 当加载器、插件、其他构建时脚本或其他动态依赖项发生更改时,
// hard-source需要替换缓存以确保输出正确。
// environmentHash被用来确定这一点。如果散列与先前的构建不同,则将使用新的缓存
environmentHash: {
root: process.cwd(),
directories: [],
files: ['./../package-lock.json', './../package.json'],
},
// An object. 控制来源
info: {
// 'none' or 'test'.
mode: 'none',
// 'debug', 'log', 'info', 'warn', or 'error'.
level: 'debug',
},
// Clean up large, old caches automatically.
cachePrune: {
// Caches younger than `maxAge` are not considered for deletion. They must
// be at least this (default: 2 days) old in milliseconds.
maxAge: 2 * 24 * 60 * 60 * 1000,
// All caches together must be larger than `sizeThreshold` before any
// caches will be deleted. Together they must be at least this
// (default: 50 MB) big in bytes.
sizeThreshold: 100 * 1024 * 1024
},
})
],
stats: {
// 去掉mini-css-extract-plugin报的日志,不然太多了
children: false,
warningsFilter: (warning) => /Conflicting order between/gm.test(warning)
},
output: {
filename: utils.assetsPath(`js/[name].[chunkhash].js`),
chunkFilename: utils.assetsPath(`js/[id].[chunkhash].js`),
// publicPath: "https://cdn.example.com/assets/",
path: path.resolve(__dirname, '../dist')
}
}
module.exports = merge(commomConfig, prodConfig)
优化前数据:
1,webpack版本与打包速度99s:
2,运行速度58s
3,热更新3.7s
4,打包体积
另一个h5大项目(同样webpack2)打包体积
优化后数据:
首次打包66.4s
缓存后二次打包23s左右
首次运行47s
缓存后二次运行9s
热更新1.6s
4,单个包体积显著减少,打包总体积减少2MB多
另一个h5大项目(同样webpack2 => webpack4)打包体积减少将近20MB
参考链接:
webpack
webpack4与它适配的loader版本记录_好 耶的博客-CSDN博客_webpack4.42.0 file-loader 版本列表
webpack中hash、chunkhash、contenthash区别 - 猴子猿 - 博客园 keepAlive组件热更新问题
vue项目 webpack构建时清除控制台无用输出信息_灰小小小熊的博客-CSDN博客_webpack 清空控制台的报错
【Webpack】538- 打包速度提升指南_pingan8787的技术博客_51CTO博客
npm
vue
css/js/vue,兼容性,压缩,抽离
webpack4:提取、压缩css(公共部分)、消除多余css_webpack4 提取公共css_飞翔的熊blabla的博客-CSDN博客
删除mini-css-extract-plugin警告_superYe7的博客-CSDN博客_webpack 清除minicss日志
Conflicting order. Following module has been added:_tongwandouQX的博客-CSDN博客
webpack4处理css兼容_风落不归处的博客-CSDN博客_webpack4 css配置
webpack使用postcss,为css自动增加浏览器前缀_postcss-loader v4_我有一棵树的博客-CSDN博客
terser-webpack-plugin的使用:删除注释和console_terserplugin_有蝉的博客-CSDN博客
多线程分析使用说明
speed-measure-webpack-plugin 是一款统计 webpack 打包时间的插件,不仅可以分析总的打包时间,还能分析各阶段loader 的耗时。
引入插件
安装 npm install --save-dev speed-measure-webpack-plugin
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
plugins: [
new SpeedMeasurePlugin()
],
运行项目后控制台输出数据
打包运行总耗时64.62s,根据耗时情况将耗时大的sass,babel,vue loader使用多线程打包优化处理,我这里使用的是webpack推荐的thread-loader;安装后在loader处理那直接加上就行,如vueloader下使用:
module: {
rules: [
/*
'thread-loader'
开启多进程打包。
进程启动大概为600ms,进程通信也有开销。
只有工作消耗时间比较长,才需要多进程打包
*/
{
test: /\.vue$/,
use: [
'thread-loader',
{
loader: 'vue-loader',
options: {
name: '[name].[hash:7].[ext]',
outputPath: utils.assetsPath('js/')
}
},
// 'thread-loader'
]
}
]
}
再次运行后耗时43.44s,效果显著。注意:'thread-loader'开启多进程打包。 进程启动大概为600ms,进程通信也有开销。只有工作消耗时间比较长,才需要多进程打包,每个项目情况不一样,只能尝试得出最优解
多进程开启后数据
ps:有没有大项目webpack转vite成功的大神分享一下经验,我们这还有一个项目打包进度展示19000+模块,webpack4优化做到极致了,热更新还是平均需要10s,开发体验较差,想转vite试试