【万字解析】Webpack 优化构建性能(分析->优化)-3. 优化构建性能

时间:2024-01-21 18:12:49

升级版本

使用最新版本的 Webpack 和 Node.js,因为高版本的 Webpack、Node.js 在内置的 API、算法上都更优。


优化搜索时间

缩小文件搜索范围

Webpack 打包时,会从配置的 entry 出发,解析入口文件的导入语句,再递归的解析,在遇到导入语句时 Webpack 会做两件事情:

  1. 根据导入语句去寻找对应的要导入的文件
  2. 根据找到的要导入文件的后缀,使用配置中的 loader 去处理文件

优化 Loader 配置

可以通过 testincludeexclude 来限制 Loader 要应用的文件.

  • test:配置要解析的文件类型
  • include:配置要解析的文件
  • exclude:配置不解析的文件
// webpack.config.js

module.exports = {
  module: {
    rules: [
      {
        // 匹配 js、mjs 文件
        test: /\.m?js$/,
        // 排除 node_modules、bower_components 目录下的文件搜索
        exclude: /(node_modules|bower_components)/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env']
          }
        }
      }
    ]
  }
}

优化 resolve.modules 配置

resolve.modules 用于配置 webpack 解析模块时应该搜索的目录。

resolve.modules 的默认值是 ['node_modules'] ,含义是先去当前目录下的 ./node_modules 目录下去找想找的模块,如果没找到就去上一级目录 ../node_modules 中找,再没有就去 ../../node_modules 中找,以此类推。

  • 相对路径:如果当前目录下没有 node_modules,则递归到父目录下查找,直到找到
  • 绝对路径:只查找当前目录,不递归
// webpack.config.js

module.exports = {
  resolve: {
    // 将 node_modules 目录下的文件视为 module
    modules: [path.resolve(__dirname, 'node_modules')],
  },
};

优化 resolve.alias 配置

resolve.alias 配置项通过别名来把原导入路径映射成一个新的导入路径,减少耗时的递归解析操作。

// webpack.config.js

const path = require('path');

module.exports = {
  resolve: {
    alias: {
      // 将当前目录下的 src 目录配置别名为 @
      '@': path.resolve(__dirname, 'src'),
    },
  },
};

减少不必要的编译工作

优化 resolve.extensions 配置

导入语句没带文件后缀时,webpack 会根据 resolve.extension 自动带上后缀后去尝试询问文件是否存在。

// webpack.config.js

module.exports = {
  resolve: {
    // 在解析未带后缀的文件时,会按照 .js -> .css 的顺序进行查找
    extensions: ['.js', '.css'],
  },
};

优化 resolve.mainFields 配置

有一些第三方模块会针对不同环境提供几份代码。 Webpack 会根据 mainFields 的配置去决定优先采用哪份代码

// webpack.config.js

module.exports = {
  resolve: {
    // 先采用 jsnext:main 的代码,再采用 main 的代码
    mainFields: ['jsnext:main', 'main'],
  },
};

优化 module.noParse 配置

module.noParse 配置项可以让 Webpack 忽略对部分没采用模块化的文件的递归解析处理,这样做的好处是能提高构建性能。
原因是一些库,例如:JQuery 、Lodash, 它们庞大又没有采用模块化标准,让 Webpack 去解析这些文件耗时又没有意义。

// webpack.config.js

module.exports = {
  module: {
    // 忽略 jquery 和 lodash 库
    noParse: /jquery|lodash/,
    // 第二种写法
    noParse: (content) => /jquery|lodash/.test(content),
  },
};

优化 resolve.symlinks 配置

如果不使用 symlinks(例如:npm link 或者 yarn link),可以设置 resolve.symlinks: false

// webpack.config.js

module.exports = {
  resolve: {
    symlinks: false
  },
};

优化 resolve.cacheWithContext 配置

如果使用自定义 resolve plugin 规则,并且没有指定 context 上下文,可以设置 resolve.cacheWithContext: false

// webpack.config.js

module.exports = {
  resolve: {
    cacheWithContext: false
  },
};

优化解析时间

多线程并行解析 - thread-Loader

  1. 安装 thread-loader

    npm i thread-loader
    
  2. 配置 webpack.config.js

    // webpack.config.js
    
    module.exports = {
      module: {
        rules: [
          {
            test: /\.js$/,
            include: path.resolve('src'),
            use: [
              "thread-loader", // 一定要放在其他 loader 后面
            ],
          },
        ],
      },
    };
    

使用资源模块替换 Loader

使用 Webpack 资源模块(asset module)代替旧的 Assets Loader(例如:file-loader/url-loader/raw-loader 等),减少 Loader 配置数量。


使用 HappyPack

HappyPack 能让 Webpack 多线程解析 Loader,它把任务分解给多个子进程去并发的执行,子进程处理完后再把结果发送给主进程。

  1. 安装 happypack

    npm i -D happypack
    
  2. 配置 webpack.config.js

// webpack.config.js

const path = require('path');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const HappyPack = require('happypack');

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        // 把对 js 文件的处理转交给 id 为 babel 的 HappyPack 实例
        use: ['happypack/loader?id=babel'],
        // 排除 node_modules 目录下的文件,node_modules 目录下的文件都是采用的 ES5 语法,没必要再通过 Babel 去转换
        exclude: path.resolve(__dirname, 'node_modules'),
      },
      {
        test: /\.css$/,
        use: ExtractTextPlugin.extract({
          // 把对 css 文件的处理转交给 id 为 css 的 HappyPack 实例
          use: ['happypack/loader?id=css'],
        }),
      },
    ]
  },
  plugins: [
    new HappyPack({
      // 用唯一的标识符 id 来代表当前的 HappyPack 是用来处理一类特定的文件
      id: 'babel',
      // 如何处理 js 文件,用法和 Loader 配置中一样
      loaders: ['babel-loader?cacheDirectory'],
    }),
    new HappyPack({
      id: 'css',
      loaders: ['css-loader'],
    }),
    new ExtractTextPlugin({
      filename: `[name].css`,
    }),
  ],
};

优化压缩时间

多线程压缩

Webpack v3
  1. 安装 webpack-parallel-uglify-plugin

    npm i -D webpack-parallel-uglify-plugin
    
  2. 配置 webpack.config.js

    // webpack.config.js
    
    const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin');
    
    module.exports = {
      plugins: [
        // 使用 ParallelUglifyPlugin 并行压缩输出的 JS 代码
        new ParallelUglifyPlugin({
          // 传递给 UglifyJS 的参数
          uglifyJS: {
            output: {
              // 最紧凑的输出
              beautify: false,
              // 删除所有的注释
              comments: false,
            },
            compress: {
              // 在 UglifyJs 删除没有用到的代码时不输出警告
              warnings: false, 
              // 删除所有的 console 语句,可以兼容 ie 浏览器
              drop_console: true,
              // 内嵌定义了但是只用到一次的变量
              collapse_vars: true,
              // 提取出出现多次但是没有定义成变量去引用的静态值
              reduce_vars: true,
            }
          },
        }),
      ],
    };
    
Webpack v4
  1. 安装 terser-webpack-plugin

    npm i terser-webpack-plugin
    
  2. 配置 webpack.config.js

    // webpack.config.js
    
    const TerserPlugin = require("terser-webpack-plugin");
    
    module.exports = {
      optimization: {
        minimize: true,
        minimizer: [
          new TerserPlugin({
            parallel: 4,
            terserOptions: {
              parse: {
                ecma: 8,
              },
              compress: {
                ecma: 5,
                warnings: false,
                comparisons: false,
                inline: 2,
              },
              mangle: {
                safari10: true,
              },
              output: {
                ecma: 5,
                comments: false,
                ascii_only: true,
              },
            },
          })
        ],
      },
    };
    

减少压缩体积

压缩 CSS - CssMinimizerWebpackPlugin
// webpack.config.js

const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");

module.exports = {
  optimization: {
    minimizer: [
      new CssMinimizerPlugin({ parallel: 4, }),
    ],
  }
}

分包 - SplitChunks
// webpack.config.js

module.exports = {
  optimization: {
    /**
     * 把 JS 文件打包成3中类型:
     * 1. vendor:第三方 lib 库,基本不会改动,除非依赖版本升级
     * 2. common:业务组件代码的公共部分抽取出来,改动较少
     * 3. entry.{page}:不同页面 entry 里业务组件代码的差异部分,会经常改动
     * 这样分的好处是尽量按改动频率来区分,利用好浏览器缓存
     */
     splitChunks: {
       chunks: 'all',
       maxInitialRequests: 4, // 防止切分粒度过细,请求过多,约束为4
       cacheGroups: {
         vendor: { // 第三方
           test: /[\\/]node_modules[\\/]/, // 打包 node_module 中的文件
           name: 'vendor', // 模块名称
           priority: 10, // 优先级 数字越大 优先级越高
           enforce: true, // 强制执行
           reuseExistingChunk: true, // 重用已有模块
         },
         common: { // 公共模块
           name: 'common',
           minChunks: 2, // 被引用两处即被归纳到公共模块
           minSize: 1, // 最小 size
           priority: 5, // 优先级
           reuseExistingChunk: true, // 重用已有模块
         },
       },
     },
     // 将 webpack 运行时生成代码打包到 runtime.js
     runtimeChunk: true,
  },
}

合并公共代码 - CommonsChunkPlugin

Webpack v4 移除,通过 optimization.splitChunks 替代。

// webpack.config.js

const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');

module.exports = {
  plugins: [
    new CommonsChunkPlugin({
      chunks: ['common', 'base'], // 从 common 和 base 两个现成的 Chunk 中提取公共的部分
      name: 'base' // 把公共的部分放到 base 中
    })
  ]
}

Tree Shaking

Tree Shaking 是指打包时移除哪些没有使用(未引入)的代码。


按需加载
  1. 代码中通过 import() 按需加载 Chunk

    // main.js
    
    window.document.getElementById('btn').addEventListener('click', function () {
      // 当按钮被点击后才去加载 show.js 文件,文件加载成功后执行文件导出的函数
      import(/* webpackChunkName: "show" */ './show').then((show) => {
        show('Webpack');
      })
    });
    
  2. webpack.config.js 中配置动态 Chunk 的输出

// webpack.config.js

module.exports = {
  entry: {
    main: './main.js',
  },
  output: {
    filename: '[name].js',
    // 为动态加载的 Chunk 配置输出文件的名称
    chunkFilename: '[name].js',
  }
};

开启 Scope Hoisting

Scope Hoisting 可以让 Webpack 打包出来的代码文件更小、运行的更快, 它又译作 “作用域提升”,是在 Webpack3 中新推出的功能。

// webpack.config.js​const ModuleConcatenationPlugin = require('webpack/lib/optimize/ModuleConcatenationPlugin');​
​
module.exports = {resolve: {// 针对 Npm 中的第三方模块优先采用 jsnext:main 中指向的 ES6 模块化语法的文件​
    mainFields: ['jsnext:main', 'browser', 'main']},plugins: [// 开启 Scope Hoisting​
    new ModuleConcatenationPlugin(),],};

优化二次打包时间

缓存

增加初次构建时间,缩短后续构建时间。

  • 利用 cache-loaderHardSourceWebpackPluginbabel-loadercacheDirectory 标志等。
  • cache
// webpack.config.js

module.exports = {
  cache: {
    type: 'filesystem',
  },
};

静态资源不再打包

DllPlugin
// webpack_dll.config.js - 构建出动态链接库文件

const path = require('path');
const DllPlugin = require('webpack/lib/DllPlugin');
const DllReferencePlugin = require('webpack/lib/DllReferencePlugin');

module.exports = {
  entry: {
    // 把 React 相关模块的放到一个单独的动态链接库
    react: ['react', 'react-dom'],
    // 把项目需要所有的 polyfill 放到一个单独的动态链接库
    polyfill: ['core-js/fn/object/assign', 'core-js/fn/promise', 'whatwg-fetch'],
  },
  output: {
    filename: '[name].dll.js',
    path: path.resolve(__dirname, 'dist'),
    // 存放动态链接库的全局变量名称,例如对应 react 来说就是 _dll_react
    // 之所以在前面加上 _dll_ 是为了防止全局变量冲突
    library: '_dll_[name]',
  },
  plugins: [
    // 接入 DllPluginnew
    DllPlugin({
      // 动态链接库的全局变量名称,需要和 output.library 中保持一致
      // 该字段的值也就是输出的 manifest.json 文件 中 name 字段的值
      // 例如 react.manifest.json 中就有 "name": "_dll_react"
      name: '_dll_[name]',
      // 描述动态链接库的 manifest.json 文件输出时的文件名称
      path: path.join(__dirname, 'dist', '[name].manifest.json'),
    }),
  ],
};
// webpack.config.js

const path = require('path');
const DllReferencePlugin = require('webpack/lib/DllReferencePlugin');

module.exports = {
  plugins: [
    // 告诉 Webpack 使用了哪些动态链接库
    new DllReferencePlugin({
      // 描述 react 动态链接库的文件内容
      manifest: require('./dist/react.manifest.json'),
    }),
    new DllReferencePlugin({
      // 描述 polyfill 动态链接库的文件内容
      manifest: require('./dist/polyfill.manifest.json'),
    }),
  ],
};

合理配置输出文件名

// webpack.config.js

module.exports = {
  output: {
    path: path.resolve(__dirname, '../dist'),
    // 给 js 文件加上 contenthash
    filename: 'js/chunk-[contenthash].js',
    clean: true,
  },
}

区分环境

  • 开发环境中,切忌在开发环境使用生产环境才会用到的工具,例如:在开发环境下,应该排除 [fullhash]/[chunkhash]/[contenthash] 等工具。
  • 生产环境中,也应该避免使用开发环境才会用到的工具,例如:webpack-dev-server 等插件。