webpack4优化

作者: Mr老朝 | 来源:发表于2019-11-06 15:13 被阅读0次

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

    一、分析工具

    • 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 type="text/javascript" src="https://cdn.bootcss.com/react/16.9.0/umd/react.production.min.js"></script>
    + ...... // 其他引用的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优化

        本文链接:https://www.haomeiwen.com/subject/prnybctx.html