更快-提高打包速度

我们可以通过speed-measure-webpack-plugin来观察webpack的打包速度

  • 分析整个打包总耗时;
  • 每个插件和 loader 的耗时情况;
    1. const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
    2. const smp = new SpeedMeasurePlugin();
    3. module.exports =smp.wrap({
    4. mode:'development',
    5. ...
    6. })
    image.png
    先回顾下webpack的流程:
    1.开始打包,我们需要获取所有的依赖模块 搜索所有的依赖项,这需要占用一定的时间,即搜索时间,那么我们就确定了:我们需要优化的第一个时间就是搜索时间。

2. 解析所有的依赖模块(解析成浏览器可运行的代码)webpack 根据我们配置的 loader 解析相应的文件。日常开发中我们需要使用 loader 对 js ,css ,图片,字体等文件做转换操作,并且转换的文件数据量也是非常大。由于 js 单线程的特性使得这些转换操作不能并发处理文件,而是需要一个个文件进行处理。我们需要优化的第二个时间就是解析时间

3. 将所有的依赖模块打包到一个文件。将所有解析完成的代码,打包到一个文件中,为了使浏览器加载的包更新(减小白屏时间),所以 webpack 会对代码进行优化。JS 压缩是发布编译的最后阶段,通常 webpack 需要卡好一会,这是因为压缩 JS 需要先将代码解析成 AST 语法树,然后需要根据复杂的规则去分析和处理 AST,最后将 AST 还原成 JS,这个过程涉及到大量计算,因此比较耗时,打包就容易卡住。我们需要优化的第三个时间就是压缩时间。

4. 二次打包 当更改项目中一个小小的文件时,我们需要重新打包,所有的文件都必须要重新打包,需要花费同初次打包相同的时间,但项目中大部分文件都没有变更,尤其是第三方库。我们需要优化的第四个时间就是二次打包时间。**

优化解析时间 - 开启多线程打包

运行在 Node.js 之上的 webpack 是单线程模式的,也就是说,webpack 打包只能逐个文件处理,当 webpack 需要打包大量文件时,打包时间就会比较漫长。

thread-loader(webpack4 官方推荐)

把这个 loader 放置在其他 loader 之前, 放置在这个 loader 之后的 loader 就会在一个单独的 worker【worker pool】 池里运行,一个worker 就是一个nodeJS 进程【node.js proces】,每个单独进程处理时间上限为600ms,各个进程的数据交换也会限制在这个时间内。

thread-loader 使用起来也非常简单,只要把 thread-loader 放置在其他 loader 之前, 那 thread-loader 之后的 loader 就会在一个单独的 worker 池(worker pool)中运行。
上面图中可以看到babel-loader耗时比较久,2.5secs。
image.png
注意:项目比较小的时候,解析时间可能会大。

合理利用缓存(缩短连续构建时间,增加初始构建时间)

HardSourceWebpackPlugin

  • 第一次构建将花费正常的时间
  • 第二次构建将显着加快(大概提升90%的构建速度) 2.63s => 1.32s
  1. plugins: [
  2. new HardSourceWebpackPlugin({
  3. // cacheDirectory是在高速缓存写入。默认情况下,将缓存存储在node_modules下的目录中
  4. // 'node_modules/.cache/hard-source/[confighash]'
  5. cacheDirectory: path.join(__dirname, './lib/.cache/hard-source/[confighash]'),
  6. // configHash在启动webpack实例时转换webpack配置,
  7. // 并用于cacheDirectory为不同的webpack配置构建不同的缓存
  8. configHash: function(webpackConfig) {
  9. // node-object-hash on npm can be used to build this.
  10. return require('node-object-hash')({sort: false}).hash(webpackConfig);
  11. },
  12. // 当加载器、插件、其他构建时脚本或其他动态依赖项发生更改时,
  13. // hard-source需要替换缓存以确保输出正确。
  14. // environmentHash被用来确定这一点。如果散列与先前的构建不同,则将使用新的缓存
  15. environmentHash: {
  16. root: process.cwd(),
  17. directories: [],
  18. files: ['package-lock.json', 'yarn.lock'],
  19. },
  20. // An object. 控制来源
  21. info: {
  22. // 'none' or 'test'.
  23. mode: 'none',
  24. // 'debug', 'log', 'info', 'warn', or 'error'.
  25. level: 'debug',
  26. },
  27. // Clean up large, old caches automatically.
  28. cachePrune: {
  29. // Caches younger than `maxAge` are not considered for deletion. They must
  30. // be at least this (default: 2 days) old in milliseconds.
  31. maxAge: 2 * 24 * 60 * 60 * 1000,
  32. // All caches together must be larger than `sizeThreshold` before any
  33. // caches will be deleted. Together they must be at least this
  34. // (default: 50 MB) big in bytes.
  35. sizeThreshold: 50 * 1024 * 1024
  36. },
  37. }),
  38. new HardSourceWebpackPlugin.ExcludeModulePlugin([
  39. {
  40. test: /.*\.DS_Store/
  41. }
  42. ]),
  43. ]

image.png

image.png

优化压缩时间

webpack4 中 webpack.optimize.UglifyJsPlugin 已被废弃。
也不推荐使用 ParallelUglifyPlugin,项目基本处于没人维护的阶段,issue 没人处理,pr没人合并。
webpack4 默认内置使用 terser-webpack-plugin 插件压缩优化代码,而该插件使用 terser 来缩小 JavaScript

terser 是什么?

所谓 terser,官方给出的定义是:

用于 ES6+ 的 JavaScript 解析器、mangler/compressor(压缩器)工具包。

为什么 webpack 选择 terser?

不再维护 uglify-es ,并且 uglify-js 不支持 ES6 +。 terser 是 uglify-es 的一个分支,主要保留了与 uglify-es 和 uglify-js@3 的 API 和 CLI 兼容性。

terser 启动多线程

使用多进程并行运行来提高构建速度。并发运行的默认数量为 os.cpus().length - 1

  1. module.exports = {
  2. optimization: {
  3. minimizer: [
  4. new TerserPlugin({
  5. parallel: true,
  6. }),
  7. ],
  8. },
  9. };

可以显著加快构建速度,因此强烈推荐开启多线程
2.63s => 2.49s

优化搜索时间- 缩小文件搜索范围 减小不必要的编译工作

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

  • 根据导入语句去寻找对应的要导入的文件。例如 require('react') 导入语句对应的文件是 ./node_modules/react/react.jsrequire('./util') 对应的文件是 ./util.js
  • 根据找到的要导入文件的后缀,使用配置中的 Loader 去处理文件。例如使用 ES6 开发的 JavaScript 文件需要使用 babel-loader 去处理。

以上两件事情虽然对于处理一个文件非常快,但是当项目大了以后文件量会变的非常多,这时候构建速度慢的问题就会暴露出来。 虽然以上两件事情无法避免,但需要尽量减少以上两件事情的发生,以提高速度。
接下来一一介绍可以优化它们的途径。

1. 优化 loader 配置

使用 Loader 时可以通过 testincludeexclude 三个配置项来命中 Loader 要应用规则的文件

2. 优化 resolve.module 配置

resolve.modules 用于配置 webpack 去哪些目录下寻找第三方模块,resolve.modules 的默认值是 ['node_modules'] ,含义是先去当前目录下的 ./node_modules 目录下去找想找的模块,如果没找到就去上一级目录 ../node_modules 中找,再没有就去 ../../node_modules 中找,以此类推。

3. 优化 resolve.alias 配置

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

4. 优化 resolve.extensions 配置

在导入语句没带文件后缀时,webpack 会根据 resolve.extension 自动带上后缀后去尝试询问文件是否存在,所以在配置 resolve.extensions 应尽可能注意以下几点:

  • resolve.extensions 列表要尽可能的小,不要把项目中不可能存在的情况写到后缀尝试列表中。
  • 频率出现最高的文件后缀要优先放在最前面,以做到尽快的退出寻找过程。
  • 在源码中写导入语句时,要尽可能的带上后缀,从而可以避免寻找过程。

    5. 优化 resolve.mainFields 配置

    有一些第三方模块会针对不同环境提供几分代码。 例如分别提供采用 ES5 和 ES6 的2份代码,这2份代码的位置写在 package.json 文件里,如下:

    1. {
    2. "jsnext:main": "es/index.js",// 采用 ES6 语法的代码入口文件
    3. "main": "lib/index.js" // 采用 ES5 语法的代码入口文件
    4. }

    webpack 会根据 mainFields 的配置去决定优先采用那份代码,mainFields 默认如下:

    1. mainFields: ['browser', 'main']

    webpack 会按照数组里的顺序去 package.json 文件里寻找,只会使用找到的第一个。
    假如你想优先采用 ES6 的那份代码,可以这样配置:

    1. mainFields: ['jsnext:main', 'browser', 'main']

    6. 优化 module.noParse 配置

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

    7. 详细配置

    1. // 编译代码的基础配置
    2. module.exports = {
    3. // ...
    4. module: {
    5. // 项目中使用的 jquery 并没有采用模块化标准,webpack 忽略它
    6. noParse: /jquery/,
    7. rules: [
    8. {
    9. // 这里编译 js、jsx
    10. // 注意:如果项目源码中没有 jsx 文件就不要写 /\.jsx?$/,提升正则表达式性能
    11. test: /\.(js|jsx)$/,
    12. // babel-loader 支持缓存转换出的结果,通过 cacheDirectory 选项开启
    13. use: ['babel-loader?cacheDirectory'],
    14. // 排除 node_modules 目录下的文件
    15. // node_modules 目录下的文件都是采用的 ES5 语法,没必要再通过 Babel 去转换
    16. exclude: /node_modules/,
    17. },
    18. ]
    19. },
    20. resolve: {
    21. // 设置模块导入规则,import/require时会直接在这些目录找文件
    22. // 可以指明存放第三方模块的绝对路径,以减少寻找
    23. modules: [
    24. path.resolve(`${project}/client/components`),
    25. path.resolve('h5_commonr/components'),
    26. 'node_modules'
    27. ],
    28. // import导入时省略后缀
    29. // 注意:尽可能的减少后缀尝试的可能性
    30. extensions: ['.js', '.jsx', '.react.js', '.css', '.json'],
    31. // import导入时别名,减少耗时的递归解析操作
    32. alias: {
    33. '@compontents': path.resolve(`${project}/compontents`),
    34. }
    35. },
    36. };

    更小-减小打包体积

    压缩JS

    由于压缩 JavaScript 代码需要先把代码解析成用 Object 抽象表示的 AST 语法树,再去应用各种规则分析和处理 AST,导致这个过程计算量巨大,耗时非常多
    webpack4 开启mode:”production”,会压缩JS代码

    压缩CSS

    extract-text-webpack-plugin: webpack3 及之前版本
    webpack4 或之后,该插件将 CSS 提取到单独的文件中。 它为每个包含 CSS 的 JS 文件创建一个 CSS 文件。 它支持 CSS 和 SourceMap 的按需加载。

  • 提取CSS

  • 压缩CSS ``` const MiniCssExtractPlugin = require(‘mini-css-extract-plugin’); const devMode = process.env.NODE_ENV !== ‘production’;

module.exports = { module: { rules: [ { test: /.css$/, use: [ devMode ? ‘style-loader’ ? MiniCssExtractPlugin.loader, ‘css-loader’ ], }, ], }, plugins: [ new MiniCssExtractPlugin({ filename: devMode ? ‘[name].css’ : ‘[name].[hash:8].css’, chunkFilename: devMode ? ‘[id].css’ : ‘[id].[hash:8].css’, disable: false, allChunks: true, }), ], };

  1. <a name="m5rVQ"></a>
  2. #### optimize-css-assets-webpack-plugin
  3. 在 webpack 构建过程中搜索 CSS ,并优化、最小化 CSS。

var OptimizeCssAssetsPlugin = require(‘optimize-css-assets-webpack-plugin’);

module.exports = { plugins: [ new OptimizeCssAssetsPlugin({ // ExtractTextPlugin 或 MiniCssExtractPlugin导出的文件名 assetNameRegExp: /.css$/g, // 用于优化/最小化CSS的CSS处理器,默认为cssnano cssProcessor: require(‘cssnano’), cssProcessorPluginOptions: { preset: [‘default’, { discardComments: { removeAll: true } }], }, // 控制插件是否可以将消息打印到控制台,默认为 true canPrint: true }) ] }; ```