webpack 优化

532 阅读6分钟

webpack

webpack 构建流程

webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:

一、初始化参数

从配置文件和shell语句中读取与合并参数,得出最终的参数

二、开始编译

用上一步得到的参数初始化Compiler(编译器)对象,加载所有配置的插件,执行对象的run方法开始执行编译

三、确定入口

根据配置中的entry找出所有的入口文件

四、编译模块

从入口文件出发,调用所有配置的loader对模块进行翻译,再找出该模块依赖的模块,再递归本步骤,直到所有入口依赖的文件都经过了本步骤的处理

五、完成模块编译

在经过第四步使用loader翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系

六、输出资源

根据入口和资源之间的依赖关系,组装成一个个包含多个模块的Chunk,再把每个Chunk转换成一个单独的文件加入到输出列表,这一步是可以修改输出内容的最后机会

七、输出完成

在确定好输出内容后,根据配置确定输出的路径和文件名,将文件内容写入到文件系统

在以上过程中,webpack会在特定的时间点广播特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,

并且插件可以调用webpack提供的API改变webpack的运行结果。

八、总结

  • 初始化: 启动构建,读取与合并配置参数,加载plugin,实例化Complier
  • 编译: 从entry出发,针对每个module串行调用对应的loader去翻译文件的内容,再找到该modle依赖的module,递归的进行编译处理
  • 输出: 将编译后的module组合成Chunk,将Chunk转换成文件,输出到文件系统中

webpack 配置优化

  • webpack在启动时会从配置的Entry出发,解析出文件中的导入语句,再递归解析依赖包
  • 对于导入语句webpack会做出以下操作:
    1. 根据导入语句寻找对应的要导入的文件
    2. 再根据要导入的文件后缀,使用配置中的loader去处理文件(如ES6需要使用babel-loader)
  • 针对这两点可以优化查找途径
  1. 优化loader配置
  • loader处理文件的转换操作时很耗时的,所以需要让尽可能少的文件被loader处理

    { test: /.js$/, use: [ 'babel-loader?cacheDirectory', // 开启转换结果缓存 ], include: path.resolve(__dirname, 'src'), // 只对src目录中文件采用babel-loader exclude: path.resolve(__dirname, './node_modules' ) // 排除node_modules目录下的文件 }

  • exclude/include 通过exclude、include配置来确保转译尽可能少的文件; exclude指定要排除的文件,include指定要包含的文件 exclude优先级高于include,在include和exclude中使用绝对路径, 尽量避免使用exclude,更倾向于使用include // webpack.config.js const path = require('path') module.export = { // ... module: { rules: [ { test: /.js[x]?$/, use: ['babel-loader'], include: [path.resolve(__dirname, 'src')] }, ... ] } }

  • cache-loader 在一些性能开销较大的loader之前添加cache-loader,将结果缓存到磁盘中; 默认保存在node_modueles/.cache/cache-loader目录下。

    1. 安装依赖 npm i cache-loader -D
    2. cache-loader的配置很简单,放在其它loader之前即可 { test: /.js[x]?$/, use: ['cache-loader','babel-loader'], include: [path.resolve(__dirname, 'src')] }
    3. 注意:
      • 如果只打算给babel-loader配置cache,可以直接给babel-loader添加选项cacheDirectory
      • 默认缓存目录: node_modules/.cache/babel-loader,
      • 开启babel-loader缓存和配置cache-loader,构建时间很接近
  1. resolve.modules 配置

不推荐使用

  • resolve.modules用于配置webpack去哪些目录下寻找第三方模块,默认是['node_modules'],但是,它会先去当前目录的./node_modules查找,没有的话再去../node_modules最后的根目录

  • 所以当安装的第三方模块都放在项目根目录时,就没有必要一层层的查找,直接指明存放的绝对位置

    resolve: { modules: [path.resolve(__dirname, 'node_modules')] }

  • 可能出现的问题

    1. 你的依赖中还存在node_modules目录,就会出现,对应的文件存在,却提示找不到,因此个人不推荐
  1. 优化resolve.extensions 配置

项目较小,优化效果不明显

  • 在导入没有文件后缀的路径时,webpack会自动带上后缀去尝试访问文件是否存在,而resolve.extensions用于配置尝试后缀列表,默认为: resolve: { extensions: ['.js', 'jsx', '.vue'] }
  • 当遇到require('./data')时webpack会先尝试寻找data.js,没有再去找data.jsx;如果列表越长,或正确的后缀越往后,尝试的次数就会越多
  • 在配置时为提升构建优化须遵守:
    1. 频率出现高的文件后缀优先放在前面
    2. 列表尽可能的小
    3. 书写导入语句时,尽可能写上后缀名
  1. HappyPack 并行构建优化

注: vue-loader不支持happypack,可以使用thread-loader来进行加速

当项目不是很复杂时,不需要配置happypack,因为进程的分配和管理也需要时间

  • 安装happypack npm i happypack -D

  • 核心原理: 将webpack中最耗时的loader文件转换操作任务,分解到多个进程中并行处理,从而减少构建时间

  • HappyPack

  • 接入HappyPack

    1. 安装: npm i -D happypack
    2. 重新配置rules部分,将loader交给happypack来配置
  • 参数:

    1. threads: 代表开启几个子进程去处理这类文件,默认3个
    2. verbose: 是否运行HappyPack输出日志,默认true
    3. threadPool: 代表共享进程池,即多个HappyPack示例使用一个共享进程池中的子进程去处理任务,以防资源占有过多
  • 注:

    1. 当postcss-loader配置在HappyPack中,必须在项目中创建postcss.config.js // postcss.config.js module.exports = { plugins: [ require('autoprefixer')() ] }
  • 示例 const HappyPack = require('happypack'); const happyThreadPool = HappyPack.ThreadPool({size: 5}); // 构建共享进程池,包含5个进程 // const os = require('os) // 开辟一个线程池 // 拿到系统CPU的最大核数,happypack 将编译工作灌满所有线程 // const happyThreadPool = HappyPack.ThreadPool({size: os.cpus().length});

    plugins: [
      // happypack 并行处理
      new HappyPack({
        // 用唯一ID来表示当前HappPack是用来处理一类特定文件的,与rules中的use对应
        id: 'babel',
        loaders: ['babel-loader?cacheDirectory'], // 默认设置loader处理
        threadPool: happyThreadPool // 使用共享进程池处理
      }),
      new HappyPack({
        id: 'css',
        loaders: [
          'css-loader',
          'postcss-loader',
          'sass-loader'
        ],
        threadPool: happyThreadPool
      }),
      
    ],
    module: {
      rules: [
        {
          test: /\.(js|jsx)$/,
          use: ['happypack/loader?id=babel'],
          exclude: path.resolve(__dirname, './node_modules')
        },
        {
          test: /\.(scss|css)$/,
            // 使用mini-css-extract-plugin 提取css此处,如果放在上面会出错
          use: [MiniCssExtractPlugin.loader, 'happypack/loader?id=css'],
          include: [
            path.resolve(__dirname, 'src'),
            path.join(__dirname, './node_modules/antd')
          ]
        },
        
        {
          rest: /\.vue$/,
          use: [
            'thread-loader',
            'vue-loader'
          ]
        }
        
      ]
    }
    
  1. ParallelUglifyPlugin代替自带UglifyJsPlugin 代码压缩插件

当前webpack使用TerserWebpackPlugin,默认就开启了多进程和缓存,构建时,你的项目中可以看到terser的缓存文件node_modules/.cache/terser-webpack-plugin。

  • 自带JS压缩插件是单线程执行,而webpack-parallel-uglify-plugin可以并行执行
  • 配置参数:
    1. uglifyJS: {} 用于压缩ES5代码时的配置
    2. test: /.js/g 使用正则去匹配哪些文件需要被压缩,默认为 /.js/
    3. include: [] 使用正则区包含被压缩的文件
    4. exclude: [] 使用正则区包含不被压缩的文件
    5. cacheDir: '' 缓存压缩后的结果
    6. workerCount: '' 开启几个子进程去并行的执行压缩,默认cpu核数减1
    7. sourceMap: false; 压缩的代码是否生成对应的Source Map module.export = { // 优化 optimization: { minimizer: [ // webpack:production模式默认有js压缩,但是如果设置了css压缩,js压缩也要重新设置 // 因为使用了minimizer会自动取消webpack的默认配置 new optimizeCssPlugin({ assetNameRegExp: /.css$/g, cssProcessor: require('cssnano'), cssProcessorOptions: {discardComments: {removeAll: true}}, canPrint: true }), new ParallelUglifyPlugin({ cacheDir: '.cache/', uglifyJS: { output: { beautify: false, // 是否输出可读性较强的代码,设置为false comments: false // 是否保留代码中的注释,false }, compress: { warnings: false, // 删除无用代码时是否输出警告信息 drop_console: true, // 是否删除代码中的console // 是否内嵌虽然已经定义了,但是只用到一次的变量, // 比如将 var x = 1; y = x, 转换成 y = 1, 默认为否 collapse_vars: true } } }) ] } }
  1. HardSourceWebpackPlugin

www.npmjs.com/package/har…

HardSourceWebpackPlugin为模块提供中间缓存,

缓存默认存放路径是: node_modules/.cache/hard-source

在首次构建时没有太大变化,但第二次开始,构建时间大约可以节约80%

  • 安装依赖 npm i hard-source-webpack-plugin -D
  • 修改webpack配置 // webpack.config.js const HardSourcePlugin = require('hard-source-webpack-plugin') module.exports = { // ... plugins: [ new HardSourceWebpackPlugin() ] }
  1. noParse

如果一些第三方模块没有AMD/CommonJS规范版本,可以使用noParse来标识这个模块;

这样webpack会引入这些模块,但不会进行转化和解析,从而提升webpack构建性能,如lodash、jquery

  • noParse属性值是一个正则表达式或是一个function // webpack.config.js module.exports = { // ... module: { noParse: /jquery|lodash/ } } 如果项目中使用了不需要解析的第三方依赖,那配置noParse很显然是一定会起到优化作用的
  1. DllPlugin

有时,如果所有的JS文件都打成一个JS文件,会导致最终生成的JS文件很大,

这时,我们就要考虑拆分bundles

  • DllPlugin和DllReferencePlugin可以实现拆分bundles,并可以大大提升构建速度
  • DllPlugin和DllReferencePlugin都是webpack的内置模块

我们使用DllPlugin将不会频繁更新的库进行编译,当这些依赖的版本没有变化时,就不需要重新编译

我们新建一个webpack的配置文件,专门用于编译动态链接库,如名为: webpack.config.dll.js

// webpack.config.dll.js
const webpack = require('webpack')
const path = require('path')
module.exports = {
  entry: {
    react: ['react', 'react-dom']
  },
  mode: 'production',
  output: {
    filename: '[name].dll.[hash:6].js',
    path: path.resolve(__dirname, 'dist', 'dll'),
    library: '[name]_dll' // 暴露给外部使用
    // libraryTarget 指定如何暴露内容,缺省时就是 var
  },
  plugins: [
    new webpack.DllPlugin({
      // name和library必须一致
      name: '[name]_dll',
      // manifest.json的生成路径
      path: path.resolve(__dirname, 'dist', 'dll', 'manifest.json')
    })
  ]
}

在package.json的scripts中增加:

{
    "scripts": {
        "dev": "NODE_ENV=development webpack-dev-server",
        "build": "NODE_ENV=production webpack",
        "build:dll": "webpack --config webpack.config.dll.js"
    },
}

执行 npm run build:all,可以看到dist目录如下:

之所以将动态链接库单独放在 dll 目录下,主要是为了使用 CleanWebpackPlugin 更为方便的过滤掉动态链接库。

dist
  --dll
    --manifest.json
    --react.dll.xxx.js

manifest.json用于让DllReferencePlugin映射到相关依赖上

修改webpack的主配置文件: webpack.config.js的配置:

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

module.exports = {
  // ...
  devServer: {
    contentBase: path.resolve(__dirname, 'dist')
  },
  plugins: [
    new webpack.DllReferencePlugin({
      manifest: path.resolve(__dirname, 'dist', 'dll', 'manifest.json')
    }),
    new CleanWebpackPlugin({
      // 不删除dll目录
      cleanOnceBeforeBuildPatterns: ['**/*', '!dll', '!dll/**']
    }),
    // ...
  ]
}

使用npm run build构建,可以看到bundle.js的体积大大减小

修改public/index.html文件,在其中引入react.dll.xxx.js

<script src="/dll/react.dll.xxx.js"></script>