基于webpack的项目分析和性能优化

663 阅读5分钟

在使用webpack进行项目构建的过程中, 当我们的项目需求累积到一定层度的时候, 总是需要做一些构建优化,以及项目结构优化, 来保证页面加载的速度, 更好的用户体验. 以下我们将通过几个方面来了解 webpack项目分析,以及更好的性能优化.

一、构建分析

1.1 打包速度分析

  • speed-measure-webpack-plugin
 npm install speed-measure-webpack-plugin -D

作用:

1. 分析整个打包总耗时

2. 每个 loader 和 plugin的耗时情况

用法:

// 导入速度分析插件
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");

// 实例化插件
const smp = new SpeedMeasurePlugin();

module.exports = {
    configureWebpack: smp.wrap({
        plugins: [
            // 这里是自己项目里需要使用到的其他插件
            new yourOtherPlugin()
        ]
    })
}

1.2 体积分析

  • webpack-bundle-analyzer
 npm install webpack-bundle-analyzer -D

作用:

分析打包之后每个文件以及每个模块对应的体积大小

用法:

// 导入速度分析插件
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");

// 导入体积分析插件
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin;

// 实例化速度分析插件
const smp = new SpeedMeasurePlugin();

module.exports = {
    configureWebpack: smp.wrap({
        plugins: [
            // 实例化体积分析插件
            new BundleAnalyzerPlugin()
        ]
    })

运行:

npm run build --report

二、性能优化

2.1 优化构建速度

2.1.1 合理使用hash值命中缓存
1. hash: 项目每次编译生成一个hash值。如果所有的文件都无变化,则hash值不变 , 反之, 则会发生改变. 所有的文件名. hash相同

2. chunkHash: 根据不同的入口文件(Entry)进行依赖文件解析、构建对应的chunk,生成对应的哈希值.  不同的chunkHash不同, 只有对应的chunk内部发生了改变, chunkHash才会改变

3. contentHash: 针对每个文件内容级别产生的哈希值, 只有文件内容发生改变了, contentHash才会改变

用法:

module.exports = {
    ...
    output: {
        // filename: 'bundle.[contentHash:8].js', 
        // 打包代码时,加上 contentHash 
        filename: '[name].[contentHash:8].js', 
    }  
    ...
}
2.1.2 减少检索范围
  • loader 使用include和exclude减少检索范围
module.exports = {
    ...
    module: {
        rules: [
            //babel-loader处理js文件, 配合的依赖有 @babel/core @@babel/preset-env, 在根目录新增.babelrc文件
            {
                test: /\.js$/,
                loader: ['babel-loader'],
                include: resolve('src'),
                exclude: /node_modules/
            },
        ]
    }
    ...
}    
  • resolve.alias减少检索路径
module.exports = {
    ...
   resolve: {
        extensions: ['.js', '.vue', '.json'],
        alias: {
            'vue$': 'vue/dist/vue.esm.js',
            '@': resolve('src'),
        }
    },
    ...
}  
 
2.1.3 优化babel-loader
1. 添加exclude配置

2. 添加.babelrc文件

3. 开启cacheDirectory 缓存

在babel-loader执行的时候,在运行期间可能会产生一些重复的公共文件,造成代码冗余,减慢编译效率:

//webpack.base.conf.js
module.exports = {
    ...
    module: {
        rules: [
            //babel-loader处理js文件, 配合的依赖有 @babel/core @@babel/preset-env, 在根目录新增.babelrc文件
            {
                test: /\.js$/,
                loader: 'babel-loader?cacheDirectory',
                include: resolve('src'),
                exclude: /node_modules/
            },
        ]
    }
    ...
} 

// .babelrc
// npm install babel-plugin-transform-runtime
{
    // "presets": [["@babel/preset-react", {"modules": false }]],
    "presets": ["@babel/preset-env", "@babel/preset-react"],
    "plugins": ["@babel/plugin-syntax-dynamic-import", "@babel/plugin-proposal-class-properties", "babel-plugin-transform-runtime"]
}
2.1.4 热更新

webpack在本地开发提升效率的最有效的办法就是使用热更新, 热更新模块时在webpack的devServer中配置:

// npm install webpack-dev-server -D

devServer: {
        contentBase: '../../dist',
        open: true,
        port: 8080,
        hot: true, //对有变化的文件打包
        hotOnly: true,
        historyApiFallback: true,
        publicPath: '/'
    }

配置 hot: true , 每次保存的时候, webpack都会对有变化的文件打包, 然后实时更新到页面

2.1.5 懒加载
  • vue异步组件
{
   path: '/index',
   component: (resolve) => {  require(['../components/index/index'], resolve) }
}
  • ES6 import() 异步引入文件
//路由
{
    path:'/index',
    component: () => import('../components/index/index')
}

{
    path:'/index',
    component: resolve => { import('../components/index/index').then(module=>resolve(module)) }
}

//es6
setTimeout(()=> {
    import('../components/index/index').then(exports =>{
        ...
    })
},1000)
  • webpack 的 require 和 ensure()
{
   path: '/index',
   component: r => require.ensure([], () => r(require('../components/index/index')), 'index')
}
2.1.6 webpack自带小插件优化
  • webpack.IgnorePlugin
// 忽略本地文件

module.exports = {
    ...
    plugins:[
       // 忽略 moment 下的 /locale 目录
        new webpack.IgnorePlugin(/\.\/locale/, /moment/),
    ]
    ...
}
  • DllPlugin和DllReferencePlugin预编译
作用: 直接使用打包好的第三方包, 不需要每次都打包

假设项目中使用了Vue, vue-router, element-ui , 首先需要出创建 webpack.dll.conf.js

const path = require('path')
const webpack = require('webpack')

module.exports = {
  entry: {
    vue: ['vue', 'vue-router'],
    ui: ['element-ui']
  },

  output: {
    path: path.join(__dirname, '../src/dll'),
    filename: '[name].dll.js',
    library: '[name]'
  },

  plugins: [
    new webpack.DllPlugin({
      path: path.join(__dirname, '../src/dll', '[name]-manifest.json'),
      name: '[name]'
    }),

    new webpack.optimize.UglifyJsPlugin()
  ]
}

然后 webpack --config ./build/webpack.dll.conf.js打包:

使用 DllReferencePlugin 在 build/webpack.base.config.js 中添加下列插件:

    ...
    plugins: [
        new webpack.DllReferencePlugin({
            manifest: path.join(__dirname, '../src/dll/ui-manifest.json')
        }),
        new webpack.DllReferencePlugin({
            manifest: path.join(__dirname, '../src/dll/vue-manifest.json')
        }),
    ],

再次执行npm run build , 比较前后两次打包的输出时间:

使用 DllPlugin 之前: npm run build

使用 DllPlugin 之后: npm run dll -> npm run build

2.1.7 多进程并行解析

多线程构建的方案比较知名的有以下三个:

1. thread-loader (推荐使用这个)

2. parallel-webpack

3. HappyPack
  • thread-loader
npm install thread-loader -D

用法:

module.exports = {
    ...
    module: {
        rules: [
            //babel-loader处理js文件, 配合的依赖有 @babel/core @@babel/preset-env, 在根目录新增.babelrc文件
            {
                test: /\.js$/,
                loader: ['thread-loader', 'babel-loader'],
                include: resolve('src'),
                exclude: /node_modules/
            },
        ]
    }
    ...
} 

//带options的配置
use: [
  {
    loader: "thread-loader",
    // loaders with equal options will share worker pools
    // 设置同样option的loaders会共享
    options: {
      // worker的数量,默认是cpu核心数
      workers: 2,

      // 一个worker并行的job数量,默认为20
      workerParallelJobs: 50,

      // 添加额外的node js 参数
      workerNodeArgs: ['--max-old-space-size=1024'],


      // 允许重新生成一个dead work pool
      // 这个过程会降低整体编译速度
      // 开发环境应该设置为false
      poolRespawn: false,


      //空闲多少秒后,干掉work 进程
      // 默认是500ms
      // 当处于监听模式下,可以设置为无限大,让worker一直存在
      poolTimeout: 2000,

      // pool 分配给workder的job数量
      // 默认是200
      // 设置的越低效率会更低,但是job分布会更均匀
      poolParallelJobs: 50,

      // name of the pool
      // can be used to create different pools with elsewise identical options
      // pool 的名字
      //
      name: "my-pool"
    }
  },
  ...
]

前后编译速度的比较, 提升了 201ms

  • HappyPack
把任务分解给多个子进程去并发的执行,子进程处理完后再把结果发送给主进程。 对file-loader、url-loader 支持的不友好
npm install happypack -D   

用法:

const HappyPack = require('happypack');
const os = require('os');
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });

module.exports = {
  ...
  module: {
    rules: [
      {
          test: /\.js$/,
          //把对.js 的文件处理交给id为happyBabel 的HappyPack 的实例执行
          use: ['happypack/loader?id=happyBabel'],
          include: resolve('src'),
          exclude: /node_modules/
      },
      ...
    ]
  },
plugins: [
    ...
    // happyPack 开启多进程打包
        new HappyPack({
            // 用唯一的标识符 id 来代表当前的 HappyPack 是用来处理一类特定的文件
            id: 'happyBabel',
            // 如何处理 .js 文件,用法和 Loader 配置中一样
            loaders: ['babel-loader?cacheDirectory'],
            // 使用共享进程池中的子进程去处理任务。
            threadPool: happyThreadPool,
            //允许 HappyPack 输出日志
            verbose: true,
        }),
  ]
}

前后编译速度的比较, 提升了 122ms

2.2 优化产出代码

2.2.1 小图片Base64编码
npm install url-loader image-webpack-loader -D

用法:

//webpack.prod.config.js

module.exports = {
    ...
  module: {
    rules: [
      {
        test: /\.(png|jpg|gif|bmp/)$/i,
        use: [
          {
             loader: 'url-loader',
             options: {
                name:'[name].[ext]',
                outputPath: 'images/',
                limit: 8192 //小于8192b,就可以转化成base64格式。大于就会打包成文件格式
            }
          },
           {
             loader:'image-webpack-loader',  //对图片资源进行压缩处理
           }
        ]
      }
    ]
  }
  ...
}
2.2.2 抽离CSS代码和压缩
使用mini-css-extract-plugin 代替 extract-text-webpack-plugin , 更好的支持异步加载,重复编译,性能更好, 只针对CSS, 代码简单, 但不支持HMR
  • mini-css-extract-plugin
作用: 将js中import的css文件提取出来,以link方式插入html, 该方式会产生额外的Http请求

版本: webpack4.0
1. npm install mini-css-extract-plugin -D

2. 在webpack.prod.config.js中:

//抽离css样式 和extract-text-webpack-plugin的差异: 支持异步加载, 不重复编译,性能更好,只针对css

const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
    ...
        module: {
        rules: [
            //抽离css
            {
                test: /\.css$/,
                loader: [
                    MiniCssExtractPlugin.loader, // 注意,这里不再用 style-loader
                    'css-loader',
                    'postcss-loader'
                ]
            },
            //抽离less -> css
            {
                test: /\.less$/,
                loader: [
                    MiniCssExtractPlugin.loader, // 注意,这里不再用 style-loader
                    'css-loader',
                    'less-loader',
                    'postcss-loader'
                ]
            }
        ]
    },
    plugins: [
        new CleanWebpackPlugin(), // 会默认清空 output.path 文件夹
        //新增生产环境变量
        new webpack.DefinePlugin({
            // window.ENV = 'production'
            ENV: JSON.stringify('production')
        }),

        //抽离css文件
        new MiniCssExtractPlugin({
            filename: 'css/main.[contentHash:8].css'
        })
    ],
    
}
  • extract-text-webpack-plugin
作用: 处理js中import的css文件 通过css-loader、style-loader、extract-text-webpack-plugin@next将js中import的css文件以link的方式插入到html

版本: webpack3.x以下
1. npm install extract-text-webpack-plugin -D

1. 在webpack.prod.config.js中:

const ExtractTextWebpackPlugin = require('extract-text-webpack-plugin');

module.exports = {
    
    module:{
        rules:[
           {
               test:/\.css$/,
               use:ExtractTextWebpackPlugin.extract({
                   fallback:'style-loader',
                   use:'css-loader'
               })
        
           }
        
        ]
        
    },
    
    plugins:[
         new ExtractTextWebpackPlugin({
              filename: '[name]-[contentHash:8].css'
         })
    ]
}
  • optimize-css-assets-webpack-plugin
npm install optimize-css-assets-webpack-plugin -D 

作用: 压缩CSS代码

用法:

//用于优化压缩css代码
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin');

module.exports = webpackMerge(webpackBaseConfig, {
    mode: 'production',
    ...
    optimization: {
        //压缩CSS文件
        minimizer: [new OptimizeCssAssetsWebpackPlugin({})],
    }

})
2.2.3 分割公共代码

使用场景:

1. 多入口文件,需要抽出公共部分的时候。
2. 单入口文件,但是因为路由异步加载对多个子chunk, 抽离子每个children公共的部分。
3. 把第三方依赖,node_modules下所有依赖抽离为单独的部分。
4. 混合使用,既需要抽离第三方依赖,又需要抽离公共部分。

在 webpack4.0 之前, 采用webpack.optimize.CommonsChunkPlugin来做代码分割, 这里做一个简单的介绍使用:

//webpack.prod.config.js

module.exports = {
    ...
    plugins:[
       new webpack.optimize.CommonsChunkPlugin({
         name: 'vendor',
         
         //minChunks 可以是数字、函数或者Infinity
         minChunks (module) {
          // any required modules inside node_modules are extracted to vendor
         return (
           module.resource &&
           /\.js$/.test(module.resource) &&
           module.resource.indexOf(
             path.join(__dirname, '../node_modules')
           ) === 0
         )
       }
      })
    ]
    ...
}

在 webpack4.0 之后, 采用config.optimization.splitChunks 实现代码分割, 抽离公共部分, 具体的使用情况如下:

常见的使用场景:

1. 抽离第三方类库(vue, vue-router等)

2. 抽离项目中相同模块引用的代码

项目结构: (采用多入口模式进行模拟)

项目代码如下:

// common/util.js
export function sum(a, b) {
    return a * b;
}


//index.js
import { sum } from './common/util'
const vue = require('vue');
const router = require('vue-router')

console.log(sum(10, 20));

//indexA.js 

import { sum } from './common/util'
console.log(sum(40, 60));
//webpack.config.js 简单配置

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const TerserWebpackPlugin = require('terser-webpack-plugin');

module.exports = {
    mode: 'production',
    entry: {
        index: path.join(__dirname, '../src/index.js'),
        indexA: path.join(__dirname, '../src/indexA.js'),
    },
    output: {
        filename: '[name].[contentHash:8].js',
        path: path.join(__dirname, '../dist')
    },
    module: {
        rules: [{
            test: /\.js$/,
            loader: 'babel-loader',
            exclude: /node_modules/,
        }]
    },
    plugins: [
        new CleanWebpackPlugin(),
        new HtmlWebpackPlugin({
            template: 'index.html',
            filename: 'index.html',
            path: path.join(__dirname, '../dist'),
            chunks: ['index']
        }),
        new HtmlWebpackPlugin({
            template: 'indexA.html',
            filename: 'indexA.html',
            path: path.join(__dirname, '../dist'),
            chunks: ['indexA']
        })
    ],


    optimization: {
        minimizer: [new TerserWebpackPlugin({})],
        splitChunks: {

            /**
             * 需要被分割的模块
             * initial 入口 chunk,对于异步导入的文件不处理
             * async 异步 chunk,只对异步导入的文件处理
             * all 全部 chunk
             */
            chunks: 'all',

            //缓存的分组
            cacheGroups: {

                //分割第三方模块
                vendor: {
                    name: 'vendor', // chunk 名称
                    priority: 1, // 权限更高,优先抽离,重要!!!
                    test: /node_modules/,
                    //minSize 规定被提取的模块在压缩前的大小最小值,单位为字节,默认为30000,只有超过了30000字节才会被提取。
                    //maxSize 把提取出来的模块打包生成的文件大小不能超过maxSize值,如果超过了,要对其进行分割并打包生成新的文件。单位为字节,默认为0,表示不限制大小
                    minSize: 0, // 大小限制
                    //
                    minChunks: 1 // 最少复用过几次
                },

                //抽离共同代码模块
                common: {
                    name: 'common', // chunk 名称
                    priority: 0, // 优先级
                    minSize: 0, // 公共模块的大小限制
                    minChunks: 2 // 公共模块最少复用过几次 index.js和indexA.js相同部分
                }
            }
        }
    }
}

编译后文件:

在整个构建过程中, 可以通过设置 minChunks 的限制来抽离共同代码, 控制common文件的生成, 因为minSize 默认为 3kb , 所以为了测试这个场景, 可以设置为 0

2.2.4 多进程并行压缩

由于 JS 是单线程, 为此可以通过开启多进程的方式, 来加快压缩效率; 目前使用并行压缩比较主流的三个方案如下:

1. 使用webpack-parallel-uglify-plugin, 一般搭配happyPack使用

2. 使用 uglifyjs-webpack-plugin 开启parallel 参数

3. 使用terser-webpack-plugin 开启 parallel 参数 (推荐使用这个,支持 ES6 语法压缩)
  • webpack-parallel-uglify-plugin
npm install webpack-parallel-uglify-plugin -D
//webpack.config.js配置 
const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin');
module.exports = {
     ...
     plugins:[
        // 使用 ParallelUglifyPlugin 并行压缩输出的 JS 代码
        new ParallelUglifyPlugin({
            // 传递给 UglifyJS 的参数
            // (还是使用 UglifyJS 压缩,只不过帮助开启了多进程)
            uglifyJS: {
                output: {
                    beautify: false, // 最紧凑的输出
                    comments: false, // 删除所有的注释
                },
                compress: {
                    // 删除所有的 `console` 语句,可以兼容ie浏览器
                    drop_console: true,
                    // 内嵌定义了但是只用到一次的变量
                    collapse_vars: true,
                    // 提取出出现多次但是没有定义成变量去引用的静态值
                    reduce_vars: true,
                }
            }
        })
     ]
     ...
}
  • uglifyjs-webpack-plugin
npm install uglifyjs-webpack-plugin -D
//webpack.config.js配置 
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
module.exports = {
     ...
     optimization: {
        minimizer: [new UglifyJsPlugin()],
     }
     ...
}


//options配置项:
module.exports = {
 ...
  optimization: {
    minimizer: [
      new UglifyJsPlugin({
        test: /\.js(\?.*)?$/i,  //测试匹配文件,
        include: /\/includes/, //包含哪些文件
        excluce: /\/excludes/, //不包含哪些文件

        //允许过滤哪些块应该被uglified(默认情况下,所有块都是uglified)。 
        //返回true以uglify块,否则返回false。
        chunkFilter: (chunk) => {
            // `vendor` 模块不压缩
            if (chunk.name === 'vendor') {
              return false;
            }
            return true;
          }
        }),
  
        cache: false,   //是否启用文件缓存,默认缓存在node_modules/.cache/uglifyjs-webpack-plugin.目录
        parallel: true,  //使用多进程并行运行来提高构建速度
    ],
  },
  ...
};
  • terser-webpack-plugin
npm install terser-webpack-plugin -D
//webpack.config.js配置 
const TerserWebpackPlugin = require('terser-webpack-plugin');
module.exports = {
     ...
     optimization: {
        minimizer: [new TerserWebpackPlugin({ 
         //    parallel:2
        })],
     }
     ...
}