webpack优化之——构建速度提升

3,751 阅读6分钟

【前言】

构建速度想必也是大家平常开发的一个痛点。毕竟比如前端同学如果需要频繁给后端同学提供一个版本,那么高效的构建速度肯定是众望所归的。

提升webpack构建速度大方向上无非2点:

  1. 减少工作量,需要打包的东西越少越好。
  2. 增加打包效率,多线程一起来。

网上翻阅了一些资料:看到提升构建速度大概如下5块:

1. 多入口情况下,通过splitchunkplugin(commonchunkplugin)提取公共模块

2. 通过配置项extenals提取常用库

3. 通过配置项Happypack提升打包速度

4. 通过DllPlugin和DllReferencePlugin提取公共库

5. 通过webpack-uglify-parallel来提升uglifyPlugin的压缩速度

在介绍这5个东西的使用之前先介绍一个可以测试webpack各个模块打包速度的插件——speed-measure-webpack-plugin 首先当然是安装:

cnpm install --save-dev speed-measure-webpack-plugin

接着在webpack的配置项中:

const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');//分析打包速度的插件
const smp = new SpeedMeasurePlugin();
module.exports = smp.wrap(webpackConfig)

其中webpackConfig就是你的webppack配置。 看下效果:


 SMP  ⏱
General output time took 16.99 secs

 SMP  ⏱  Plugins
FasterUglifyPlugin took 7.7 secs
ExtractTextPlugin took 1.026 secs
OptimizeCssAssetsPlugin took 0.805 secs
HappyPlugin took 0.372 secs
HtmlWebpackPlugin took 0.268 secs
Object took 0.014 secs
ModuleConcatenationPlugin took 0.006 secs
HashedModuleIdsPlugin took 0.005 secs
CommonsChunkPlugin took 0.004 secs
DefinePlugin took 0 secs

 SMP  ⏱  Loaders
_happypack@5.0.1@happypack took 5.47 secs
  module count = 12
modules with no loaders took 3.17 secs
  module count = 273
_extract-text-webpack-plugin@3.0.2@extract-text-webpack-plugin, and
_vue-style-loader@3.1.2@vue-style-loader, and
_css-loader@0.28.11@css-loader, and
_postcss-loader@2.1.6@postcss-loader took 3.09 secs
  module count = 3
_css-loader@0.28.11@css-loader, and
_postcss-loader@2.1.6@postcss-loader took 2.7 secs
  module count = 3
_babel-loader@7.1.5@babel-loader, and
_vue-loader@13.7.3@vue-loader took 1.44 secs
  module count = 9
_extract-text-webpack-plugin@3.0.2@extract-text-webpack-plugin, and
_vue-style-loader@3.1.2@vue-style-loader, and
_css-loader@0.28.11@css-loader, and
_vue-loader@13.7.3@vue-loader, and
_less-loader@4.1.0@less-loader, and
_vue-loader@13.7.3@vue-loader took 0.986 secs
  module count = 10
_vue-loader@13.7.3@vue-loader, and
_vue-loader@13.7.3@vue-loader took 0.607 secs
  module count = 9
_css-loader@0.28.11@css-loader, and
_vue-loader@13.7.3@vue-loader, and
_less-loader@4.1.0@less-loader, and
_vue-loader@13.7.3@vue-loader took 0.461 secs
  module count = 10
_json-loader@0.5.7@json-loader took 0.132 secs
  module count = 6
_url-loader@0.5.9@url-loader took 0.046 secs
  module count = 13
_html-webpack-plugin@2.30.1@html-webpack-plugin took 0.023 secs
  module count = 1



Hash: ca5a77aa6530bee1edf6
Version: webpack 3.12.0
Time: 17364ms
                                                  Asset       Size  Chunks                    Chunk Names
          static/fonts/fontawesome-webfont.fee66e7.woff      98 kB          [emitted]
           static/fonts/fontawesome-webfont.674f50d.eot     166 kB          [emitted]
           static/fonts/fontawesome-webfont.b06871f.ttf     166 kB          [emitted]
             static/img/fontawesome-webfont.912ec66.svg     444 kB          [emitted]  [big]
         static/fonts/fontawesome-webfont.af7ae50.woff2    77.2 kB          [emitted]
                      static/fonts/fontello.e73a064.eot    15.6 kB          [emitted]
                      static/fonts/fontello.068ca2b.ttf    15.4 kB          [emitted]
                        static/img/fontello.9354499.svg    16.2 kB          [emitted]
                         static/img/home_bg.d20da16.jpg     175 kB          [emitted]
                    static/js/0.d0284f664931a06e03e7.js    3.44 kB       0  [emitted]
               static/js/vendor.7513fa59024078b2b337.js     678 kB       1  [emitted]  [big]  vendor
                  static/js/app.1b34212681dbab2869f4.js      36 kB       2  [emitted]         app
             static/js/manifest.f2953cc5ea49b3673395.js    1.44 kB       3  [emitted]         manifest
    static/css/app.196e9c842ef4b8e4f1df90375df08b59.css    77.6 kB       2  [emitted]         app
static/css/app.196e9c842ef4b8e4f1df90375df08b59.css.map     117 kB          [emitted]
                static/js/0.d0284f664931a06e03e7.js.map    23.7 kB       0  [emitted]
           static/js/vendor.7513fa59024078b2b337.js.map    2.14 MB       1  [emitted]         vendor
              static/js/app.1b34212681dbab2869f4.js.map     146 kB       2  [emitted]         app
         static/js/manifest.f2953cc5ea49b3673395.js.map    7.75 kB       3  [emitted]         manifest
                                             index.html  864 bytes          [emitted]

  Build complete.

如此一来,你就可以看到webpack每个loader和plugin花去的打包时间,从而针对性的去优化。

下面逐一介绍些上面说的5个东西的使用:

splitchunkplugin

splitchunkplugin这个插件在webpack4.x之前叫commonchunkplugin,而且需要安装,在4之后变为开箱即用的插件,像vue-cli提供的webpack默认配置就帮你默认设置好了,基本不用动。原理就是多入口的情况下位了不重复打包公共模块,提取了公共部分以供复用。具体使用可以参考我这篇文章

extenals

这是webpack配置项的一个选项,它的值是一个对象:

    externals: {
        jQuery: "jquery",
        $: "jquery",
        echarts: "echarts",
        ElementUI: "element-ui",
        Vue: "vue"
    },

当你设置好这些值后,webpack打包的时候就会把这些库忽略。这个时候你就需要通过其他方式把这些库引进来比如cdn,比如DllPluginDllReferencePlugin

例子:

body里面头2个script标签就是弄好的cdn和Dll

<!DOCTYPE html>
<html>

<head>
    <meta charset=utf-8>
    <meta name=viewport content="width=device-width,initial-scale=1">
    <title>iwangcx</title>
    <link rel=stylesheet href=https://unpkg.com/element-ui/lib/theme-chalk/index.css>
    <link href=/static/css/app.196e9c842ef4b8e4f1df90375df08b59.css rel=stylesheet>
</head>

<body>
    <div id=app></div>
    <script src=https://cdn.jsdelivr.net/npm/vue></script>
    <script src="./vendor.dll.js"></script><script></script>
    <script src=https://unpkg.com/element-ui/lib/index.js></script>
    <script src=https://cdn.bootcss.com/jquery/3.4.0/jquery.min.js></script>
    <script src=https://cdn.bootcss.com/echarts/4.2.1-rc1/echarts.common.min.js></script>
    <script type=text/javascript src=/static/js/manifest.f2953cc5ea49b3673395.js></script>
    <script type=text/javascript src=/static/js/vendor.7513fa59024078b2b337.js></script>
    <script type=text/javascript src=/static/js/app.1b34212681dbab2869f4.js></script>
</body>

</html>

Happypack

这东西据说是对大型项目有显著的提升作用。为什么说据说,因为我项目都太小,暂时还无法看到它带来的收益。并且通过happpack创建进程和进行进程间的通信都是有开销的,所以一般的项目反而是增加了打包时间。这里简单讲下它的使用。

先安装:

cnpm install happypack --save-dev

接着:

找到你需要使用happypack优化的loader:


            {
                test: /\.js$/,
                exclude: /node_modules/,
                use: [
                    // {
                    //     loader: 'babel-loader',
                    //     options: {
                    //         presets: [[
                    //             '@babel/preset-env',
                    //             {
                    //                 "modules": false,
                    //                 "useBuiltIns": "usage",
                    //                 "corejs": 2
                    //             }
                    //         ]],
                    //         // plugins: ['@babel/plugin-transform-runtime']
                    //     }
                    // }
                    'happypack/loader?id=babel-loader'

                ]
            },

注释掉之前的内容,换成'happypack/loader?id=babel-loader,其中id要和下面plugin中的id保持一致,loader的配置项照搬:

    plugins: [

        new HappyPack({
            id: 'babel-loader',
            loaders: [
                {
                    loader: 'babel-loader',
                    options: {
                        presets: [[
                            '@babel/preset-env',
                            {
                                "modules": false,
                                "useBuiltIns": "usage",
                                "corejs": 2
                            }
                        ]]
                    }
                }
            ]
        })
        ]

至此,配置完毕,虽然对我这种小项目没什么效果。。。

DllPlugin 和 DllReferencePlugin

这2个东西可能是本文中唯一一个带来明显收益的,尤其是没有CDN的公共模块!

它的原理是这样的:

  1. 利用webpack的配置文件先创建一个需要提前打包后的js文件,并创建一个manifest.json,这个manifest.json就想相当于一个字典,通过这个manifest.json可以去提前打包好的js文件里去拿需要的模块。(只要需要的模块不变动,就不需要再次使用这个配置文件去打包了!)
  2. 在真正的webpack配置文件里去引入DllReferencePlugin,这个插件会自动去按照指定的manifest.json去拿相关模块(用这个配置进行webpack打包那就开始相当快了。。)。

这样一来,那些公共模块不用每次打包了,就加速了打包的过程。

在项目目录下新建一个名为webpack.dll.config.js的webpack配置文件,主要是用来生成打包的JS文件。


//webpack.config.js
const path = require('path');


const webpack = require('webpack');

module.exports = {
    entry: {
        vendor: ['jquery']//这个vendor就会作为下面的[name],数组里是需要打包的模块,require(xxx)里面xxx怎么写,它就怎么写
    },
    output: {
        filename: '[name].dll.js',//最后打包生成作为库的JS文件
        path: path.resolve(__dirname, 'dist'),
        library: '[name]_dll_[hash]'//这是打包后文件内部的变量名,之后的manifest就是根据这个名字查找相关模块的。
    },
    // resolve: {
    //     extensions: ['js', 'jsx', 'json']
    // },
    mode:"development",
    plugins: [
        // new webpack.ProvidePlugin({
        //     jQuery: 'jquery'
        // }),
        new webpack.DllPlugin({
            // context: path.join(__dirname, 'dist'),
            // context: __dirname,
            name: '[name]_dll_[hash]',//这个名字要和上面的library保持一致用于查找的。
            path: path.join(__dirname, 'dist', "[name].manifest.json")//生成的manifest的文件目录和名称
        })
    ]
}

然后在package.json里面写个scrpt

{
  "name": "webpack-test",
  "version": "1.0.0",
  "description": "",
  "private": true,
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build_dll": "webpack --config webpack.dll.config.js",
    "build": "webpack"
  },

执行

npm run build_dll

这样你就可以在dist目录下看到生成的vendor.dll.js文件和vendor.manifest.json文件了。

之后在你真正要打包的webppack文件里,比如叫webpack.config.js

//webpack.config.js

const path = require('path');

const HtmlWebpackPlugin = require('html-webpack-plugin');
const MyPlugin = require('my-plugin');

const webpack = require('webpack');

module.exports = {
    entry: ['./src/index.js'],
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, 'dist')
    },
    mode:"development",
    module: {
        rules: [
            {
                test: /\.css$/,
                exclude: /node_modules/,
                use: [
                    'style-loader',
                    {
                        loader: 'css-loader',
                        options: {
                            importLoaders: 2,
                            // localsConvention: '[name]--[local]--[hash:base64:5]',
                            sourceMap: true,
                            modules: true  //重点                            
                        }
                    }
                ]

            },
            {
                test: /\.js$/,
                exclude: /node_modules/,
                use: [
                    {
                        loader: 'babel-loader',
                        options: {
                            presets: [[
                                '@babel/preset-env',
                                {
                                    "modules": false,
                                    "useBuiltIns": "usage",
                                    "corejs": 2
                                }
                            ]],
                            // plugins: ['@babel/plugin-transform-runtime']
                        }
                    }
                ]
            }, 
            {
                test: /\.txt$/,
                exclude: /node_modules/,
                use: [
                    {
                        loader: 'my-loader', 
                        options: {
                            name: 'my-loader'
                        }
                    }
                ]
            }
        ]
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: './index.html'
        }),
        new MyPlugin(),
        // new webpack.ProvidePlugin({
        //     jQuery: 'jquery'
        // })
        new webpack.DllReferencePlugin({
            // context: __dirname,
            manifest: require('./dist/vendor.manifest.json')
            //content:xxxx,
            // name: 'vendor'
            // scope: 'vendor',
            // sourceType: 'commonjs2',
            // extensions: ['js', 'jsx', 'json']
        })        
    ]
}

最后看下通过HtmlWebpackPlugin生成的index.html文件。

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <script src="./vendor.dll.js"></script>
    <script src="bundle.js"></script>
</body>

</html>

记住这个vendor.dll.js千万别忘了引入!

然后你在weppack命令行页面会发现,打包的内容大大减少了。。。。

webpack-uglify-parallel

这个东西我暂时不清楚为什么要用它,它的原理也是利用了多线程,但是!原先的uglifyjs-webpack-plugin不是自带多线程的功能么?:

    new UglifyJsPlugin({
      uglifyOptions: {
        compress: {
          warnings: false
        }
      },
      sourceMap: config.build.productionSourceMap,
      parallel: true//多线程
    }),

参考文献

www.cnblogs.com/gaoht/p/113…