webpack4优化

下面是我对一个庞大的多页面项目优化的总结,有些评论仅代表我在优化过程遇到的。优化方法、用法我都列举了,望君自行斟酌取舍

一、分析工具

  • 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,它能够测量出在你的构建过程中,每一个 LoaderPlugin的执行时长
  • 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、 babelloaders解析阶段
  • 2、 jscss压缩阶段

(一)缓存

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
        })
  • 最难的是找到一个适当的拆分设置,上面的设置仅供参考
  • 适当的拆分,可以优化整个打包文件的大小
  • 适当的拆分,可以优化开发环境热更新的速度
  • webpack4CommonsChunkPlugin废弃,由optimization.splitChunksoptimization.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.jsmanifest.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-pluginhtml插入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" // 后面会介绍
+        },
+    ]
+ }
  • 减少不必要的编译,即modulesmainFieldsnoParseincludesexcludealias都用起来
  • 2、tree-shaking
    tree shaking设计的初衷应该是shaking掉第三方引入的样式中无用的代码。业务代码,尤其像.vue这样的组件化开发tree shaking的使用有限。反正我打开后各种问题,就弃坑了
// package.json

    ......
    sideEffects: false
    ......
  • 设置sideEffects: false告诉编译器该项目或模块是pure的(所有文件都没有副作用),可以进行无用模块删除
// package.json

"sideEffects": [
    "*.css*",
    "*.vue"
],
  • .css文件、.vue文件模块有副作用,需要在打包的时候不要错误删除了这些模块的代码

四、总结

打完收工。

你可能感兴趣的:(webpack4优化)