记一次webpack2到webpack4的升级记录(打包运行速度优化,webpack依赖插件版本,webpack生产打包配置文件)

325 阅读4分钟

我们的后台系统是前端单页应用(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:

b92869f71228b93ae2812986331f67d.png

2,运行速度58s

image.png

3,热更新3.7s

8532fea05575bae945d3a770e5e8029.png

4,打包体积

image.png

另一个h5大项目(同样webpack2)打包体积

image.png

优化后数据:

首次打包66.4s

image.png

缓存后二次打包23s左右

image.png

首次运行47s

image.png

缓存后二次运行9s

image.png

热更新1.6s

3c944896b514642838fb4b3fdeb4734.png

4,单个包体积显著减少,打包总体积减少2MB多

image.png

另一个h5大项目(同样webpack2 => webpack4)打包体积减少将近20MB

image.png

参考链接:

webpack

webpack4与它适配的loader版本记录_好 耶的博客-CSDN博客_webpack4.42.0 file-loader 版本列表

webpack中hash、chunkhash、contenthash区别 - 猴子猿 - 博客园 keepAlive组件热更新问题

vue项目 webpack构建时清除控制台无用输出信息_灰小小小熊的博客-CSDN博客_webpack 清空控制台的报错

前端构建效率优化之路 - 腾讯云开发者社区-腾讯云

【Webpack】538- 打包速度提升指南_pingan8787的技术博客_51CTO博客

(10条消息) webpack4性能优化小结——HMR、source-map、oneOf、babel缓存、 多进程打包、文件缓存、tree shaking、code split、dll、pwa、externals_写个毛代码的博客-CSDN博客_webpack4性能优化

npm

npm ERR! code 128npm ERR! An unknown git error occurrednpm ERR! command git --no-replace-objects l_npm err! code 128 npm err! an unknown git error oc_郭宝的博客-CSDN博客

vue

使用低版本的vue-loader遇到的坑之ValidationError: Invalid options object. CSS Loader has been initialized using_qq_41687400的博客-CSDN博客_validationerror: invalid options object. css loade

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博客

splitChunks代码分割(IT技术)

SplitChunksPlugin 插件快速入门 - 知乎

多线程分析使用说明

speed-measure-webpack-plugin 是一款统计 webpack 打包时间的插件,不仅可以分析总的打包时间,还能分析各阶段loader 的耗时。

引入插件
安装 npm install --save-dev speed-measure-webpack-plugin
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
plugins: [
   new SpeedMeasurePlugin()
],
运行项目后控制台输出数据

image.png

打包运行总耗时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,进程通信也有开销。只有工作消耗时间比较长,才需要多进程打包,每个项目情况不一样,只能尝试得出最优解

多进程开启后数据

image.png

ps:有没有大项目webpack转vite成功的大神分享一下经验,我们这还有一个项目打包进度展示19000+模块,webpack4优化做到极致了,热更新还是平均需要10s,开发体验较差,想转vite试试