Webpack5构建速度提升令人惊叹,早升级早受益

 为什么要升级?

webpack4用的好好的,运行稳定,为什么要升级到webpack5, 每次升级,都要经历一场地震,处理许多loader和plugin API的破坏性改变。 请给我们一个充分的升级理由,不然真的没有动力去折腾。没问题,给你们一个充分的理由,webpack5对构建速度做了突破性的改进,开启文件缓存之后,再次构建,速度提升明显。在我参与的项目中,本地服务器开发环境,第一次构建速度是38.64s,第二次构建速度是1.69s,提升了一个数量级。My God, 是不是很惊喜,很意外。

 生产打包构建速度,同样有显著提升,第一次打包耗时1.01m,第二次打包耗时10.95s.  看到这里,你是不是有了升级的热情,那请继续往下看。

 

 为什么构建速度有了质的飞跃?

主要是因为:

1.webpack4是根据代码的结构生成chunkhash,添加了空白行或注释,会引起chunkhash的变化,webpack5是根据内容生成chunkhash,改了注释或者变量不会引起chunkhash的变化,浏览器可以继续使用缓存。

2.优化了对缓存的使用效率。在webpack4 中,chunkId与moduleId都是自增id。只要我们新增一个模块,那么代码中module的数量就会发生变化,从而导致moduleId发生变化,于是文件内容就发生了变化。chunkId也是如此,新增一个入口的时候,chunk数量的变化造成了chunkId的变化,导致了文件内容变化。所以对实际未改变的chunk文件不能有效利用。webpack5采用新的算法来计算确定性的chunkId和moduleId。可以有效利用缓存。在production模式下,optimization.chunkIds和optimization.moduleIds默认会设为’deterministic’。

3.新增了可以将缓存写入磁盘的配置项, 在命令行终止当前构建任务,再次启动构建时,可以复用上一次写入硬盘的缓存,加快构建过程。

这两项的默认配置为:

module.exports = (env) => {
  return {
    splitChunks: {
        chunks: 'async',         // 指明要分割的插件类型, async:异步插件(动态导入),inital:同步插件,all:全部类型
        minSize: 20000,          //文件最小大小,单位bite;即超过minSize有可能被分割;
        minRemainingSize: 0,     // webpack5新属性,防止0尺寸的chunk
        minChunks: 1,            // 被提取的模块必须被引用1次
        maxAsyncRequests: 30,    // 异步加载代码时同时进行的最大请求数不得超过30个
        maxInitialRequests: 30,  // 入口文件加载时最大同时请求数不得超过30个
        enforceSizeThreshold: 50000,
        cacheGroups: {
          // 分组缓存
          // 将来自node_modules的模块提取到一个公共文件中 (由v4的vendors改名而来)
          defaultVendors: {
            test: /[\\/]node_modules[\\/]/,
            priority: -10,
            reuseExistingChunk: true,
          },
          default: {
            minChunks: 2,
            priority: -20,
            reuseExistingChunk: true,
          },
        },
      },
    },
};

开启升级之旅

webpack每个大版本的升级,都是破坏性变革,很少向后兼容,webpack4到webpack5的升级,同样也不例外。升级犹如去西天取经一样,需要经过九九八十一难,才能取得真经,体会到成就感。只要没有坚持到最后,就会前功尽弃。所以一定要有耐心。好了,废话不多说。现在进入这个章节的主题,细数一下升级过程中踩过的各种坑。

我对webpack的升级之旅是这样开始的, 直接在webpack4的webpack.config.js添加与提升构建速度有关的配置

module.exports = () => {
  return {
    // ...
    optimization: {
      // 此设置保证有新增的入口文件时,原有缓存的chunk文件仍然可用
      moduleIds: "deterministic",
      // 值为"single"会创建一个在所有生成chunk之间共享的运行时文件
      runtimeChunk: "single",
      splitChunks: {
        // 设置为all, chunk可以在异步和非异步chunk之间共享。
        chunks: 'all', 
        cacheGroups: {
          vendor: {
            test: /[\\/]node_modules[\\/]/,
            name: "vendors",
            chunks: "all",
          },
        },
      },
    },
    cache: {
      // 将缓存类型设置为文件系统,默认是memory
      type: "filesystem",
      buildDependencies: {
        // 更改配置文件时,重新缓存
        config: [__filename],
      },
    },
  };
};

报如下错误,webpack4 optimization.moduleIds不能设置为deterministic。

于是对webpack4进行升级, 从"webpack": "^4.39.1"升级到"webpack": "^5.36.1",,升级后,启动编译,报如下错误 configuration.devtool should match pattern "^(inline-|hidden-|eval-)?(nosources-)?(cheap-(module-)?)?source-map$"

将devtool的配置 由devtool: 'cheap-module-eval-source-map'改为devtool: 'eval-cheap-module-source-map', 继续前行,编译报如下错误:

 升级 "html-webpack-plugin": "^3.2.0"到"html-webpack-plugin": "^5.3.1",继续前行,编译报如下错误:Cannot read property 'normal' of undefined

 这次既有警告,又有报错,经查告警和报错是由于webpack5的API发生改变,而基于webpack4 API开发的一些node工具包还未同步变更, 版本与webpack5不兼容引起的,头痛医头,脚痛医脚,会事倍功半,不胜其烦。于是决定放大招,升级package.json中所有的开发时依赖到最新版本。

yarn upgrade-interactive --latest

 对标红的开发依赖包进行升级后,继续前行,编译报如下错误 Cannot find module 'webpack-cli/bin/config-yargs'

 经查,是因为webpack-cli4移除了yargs模块,除了要注释掉项目中对yargs模块的引用,还要修改package.json里面webpack-dev-server的写法, 将'webpack-dev-server'改为'webpack serve'。

    "start:local": "cross-env NODE_ENV=development webpack-dev-server --config webpack/dev.js --progress --mode development --current-env local",
    "start:dev": "cross-env NODE_ENV=development webpack-dev-server --config webpack/dev.js --progress --mode development --current-env dev",
    "start:test": "cross-env NODE_ENV=development webpack-dev-server --config webpack/dev.js --progress --mode development --current-env test",
    "start:prod": "cross-env NODE_ENV=development webpack-dev-server --config webpack/dev.js --progress --mode development --current-env prod",
    "start:local": "cross-env NODE_ENV=development webpack serve --config webpack/dev.js --progress --mode development --current-env local",
    "start:dev": "cross-env NODE_ENV=development webpack serve --config webpack/dev.js --progress --mode development --current-env dev",
    "start:test": "cross-env NODE_ENV=development webpack serve --config webpack/dev.js --progress --mode development --current-env test",
    "start:prod": "cross-env NODE_ENV=development webpack serve --config webpack/dev.js --progress --mode development --current-env prod",

改完之后,继续前行,编译报如下错误 Unknown options

 经查是因为webpack-cli的参数写法不对,于是按照官方文档修改为

    "start:local": "cross-env NODE_ENV=development webpack serve --config webpack/dev.js --progress --mode development  --env currentEnv=local",
    "start:dev": "cross-env NODE_ENV=development webpack serve --config webpack/dev.js --progress --mode development --env currentEnv=dev",
    "start:test": "cross-env NODE_ENV=development webpack serve --config webpack/dev.js --progress --mode development --env currentEnv=test",
    "start:prod": "cross-env NODE_ENV=development webpack serve --config webpack/dev.js --progress --mode development --env currentEnv=prod",

获取命令行自定义参数的写法改为

module.exports = (env) => {
  const currentEnv = env.currentEnv;
  //...
}

改完之后,继续前行,编译报如下错误:TypeError: merge is not a function

 经查是最新版本的webpack-merge的merge导出方式有问题,修改merge的导出方式为

const { merge } = require('webpack-merge');

改完之后,继续前行,编译报如下错误: this.getOptions is not a function

经查是less-loader的配置写法导致的, 按照最新版本的配置写法,修改less和module.less加载器的配置

const lessLoader = [
  "css-loader",
  "postcss-loader",
  {
    loader: "less-loader",
    options: { lessOptions: { javascriptEnabled: true } },
  },
];

module.exports = () => {
  return {
    // ...
    module: {
      rules: [
        {
          test: lessReg,
          exclude: lessModuleReg,
          use: isDev
            ? ["style-loader", ...lessLoader]
            : [MiniCssExtractPlugin.loader, "happypack/loader?id=less"],
        },
        {
          test: lessModuleReg,
          exclude: path.resolve(__dirname, "./node_modules"),
          // include: [path.resolve(__dirname, '../src')],
          use: isDev
            ? ["style-loader", ...lessLoader]
            : [
                MiniCssExtractPlugin.loader,
                "happypack/loader?id=lessWithModule",
              ],
        },
      ],
    },
  };
};

继续前行,编译有如下警告: consider using [chunkhash] or [contenthash]

将项目配置中用到hash的地方,修改成contenthash

module.exports = () => {
  return {
    // ...
    output: {
      path: path.resolve(rootPath, "./dist"),
      filename: isDev
        ? "js/[name].[contenthash:8].js"
        : "js/[name].[chunkhash:8].js",
      publicPath,
    },
    module: {
      rules: [
        {
          test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/, /\.ico$/],
          loader: "url-loader",
          options: {
            limit: 10000,
            name: isDev
              ? "image/[name][contenthash:8].[ext]"
              : "image/[name].[contenthash:8].[ext]",
          },
        },
        {
          // 添加otf字体支持
          test: /\.(woff|svg|eot|ttf|otf)\??.*$/,
          loader: "url-loader",
          options: {
            limit: 10000,
            name: isDev
              ? "font/[name][contenthash:8].[ext]"
              : "font/[name].[contenthash:8].[ext]",
          },
        },
      ],
    },
    plugins: [
      new MiniCssExtractPlugin({
        filename: isDev
          ? "css/[name][contenthash:8].css"
          : "css/[name].[chunkhash:8].css",
        chunkFilename: isDev
          ? "css/[id][contenthash:8].css"
          : "css/[id].[chunkhash:8].css",
        ignoreOrder: false,
      }),
    ],
  };
};

修改完之后,本地开发环境终于不报错了。可是发现修改代码之后页面不自动刷新。经查是webpack5的bug, 如果在 package.json 里面写了 browserslist,会导致热更新失效,解决方案是在 webpack 配置中设置 target 字段,在开发阶段使得 browserslist 失效

module.exports = (env) => {
  return {
    // ...
    target: process.env.NODE_ENV === "development" ? "web" : "browserslist",
  };
};

再看看生产编译打包是否正常。

 执行yarn build:prod之后,报如下错误 MainTemplate.hooks.hashForChunk is deprecated,这个报错前面遇到过,一看就是生产模式用到的不同于开发模式的插件,与webpack5不兼容导致的。

 经查解决方案是用 terser-webpack-plugin替换原来的js压缩插件uglifyjs-webpack-plugin

const TerserPlugin = require('terser-webpack-plugin'); // 对js进行压缩

module.exports = () => {
  return {
    // ...
    optimization: {
        minimize: true,
        minimizer: [
          // terserPlugin是webpack推荐及内置的压缩插件,cache与parallel默认为开启状态
          // 缓存路径在node_modules/.cache/terser-webpack-plugin
          new TerserPlugin({
            terserOptions: {
              // https://github.com/terser/terser#minify-options
              compress: {
                warnings: false, // 删除无用代码时是否给出警告
                drop_debugger: true, // 删除所有的debugger
                // drop_console: true, // 删除所有的console.*
                pure_funcs: [''],
                // pure_funcs: ['console.log'], // 删除所有的console.log
              },
            },
          }),
          new CssMinimizerPlugin(),
        ],
      },
  };
};

改完之后,编译报许多如下错误: You forgot to add 'mini-css-extract-plugin'

 经查是因为webpack5中,happypack不再支持less-loader,修改配置文件,less-loader不开启多进程编译

module.exports = () => {
  return {
    // ...
    module: {
      rules: [
        {
          test: lessReg,
          exclude: lessModuleReg,
          // use: isDev ? ['style-loader', ...lessLoader] : ['happypack/loader?id=less'],
          use: isDev
            ? ["style-loader", ...lessLoader]
            : [MiniCssExtractPlugin.loader, ...lessLoader],
        },
        {
          test: lessModuleReg,
          exclude: path.resolve(__dirname, "./node_modules"),
          // include: [path.resolve(__dirname, '../src')],
          // use: isDev
          //   ? ['style-loader', ...lessLoader]
          //   : ['happypack/loader?id=lessWithModule'],
          use: isDev
            ? ["style-loader", ...lessLoader]
            : [MiniCssExtractPlugin.loader, ...lessLoader],
        },
      ],
    },
    plugins: [
        // new Happypack({
        //   id: 'less',
        //   threadPool: happyThreadPool,
        //   use: [MiniCssExtractPlugin.loader, ...lessLoader],
        // }),
        // new Happypack({
        //   id: 'lessWithModule',
        //   threadPool: happyThreadPool,
        //   use: [MiniCssExtractPlugin.loader, ...lessLoader],
        // }),
      ],
  };
};

修改之后,继续编译,报如下错误:Module not found: Error: Can't resolve 'crypto'

 经查webpack4 引入crypto-js模块会自动引入polyfill: crypto-browserify, webpack5默认会自动将path、crypto、http、stream、zlib、vm的node polyfill剔除,为了不影响之前的业务,我们手动添加这个工具包

yarn add -D crypto-browserify
module.exports = () => {
  return {
    // ...
    resolve: {
        fallback:{
            "stream": false,
            "buffer": false,
            "crypto": require.resolve("crypto-browserify")
          }
      },
  };
};

改完之后,编译报如下警告: Conflicting values for 'process.env'

 经查是webpack5 定义全局变量的写法改变了,按照最新的语法修改如下:

module.exports = () => {
  return {
    // ...
    plugins: [
      // webpack5 定义环境变量的写法变了
      new webpack.DefinePlugin({
        "process.env.WX_JS_SDK_ENABLED": WX_JS_SDK_ENABLED,
        "process.env.CURRENT_ENV": JSON.stringify(currentEnv),
        "process.env.RELEASE_VERSION": JSON.stringify(RELEASE_VERSION),
      }),

      // webpack4的写法
    //   new webpack.DefinePlugin({
    //     "process.env": {
    //       WX_JS_SDK_ENABLED: WX_JS_SDK_ENABLED, // 是否真机调试SDK模式
    //       CURRENT_ENV: JSON.stringify(currentEnv),
    //       RELEASE_VERSION: JSON.stringify(RELEASE_VERSION),
    //     },
    //   }),
    ],
  };
};

修改完之后,编译报如下错误:optimizeChunkAssets is deprecated

 经查是optimize-css-assets-webpack-plugin插件与webpack5不兼容引起的警告,webpack5中同等功能的插件是css-minimizer-webpack-plugin,安装并修改配置

yarn add -D css-minimizer-webpack-plugin
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); // 对CSS进行压缩
module.exports = () => {
  return {
    // ...
    optimization: {
      minimize: true,
      minimizer: [
        // ...
        // new OptimizeCSSAssetsPlugin(),
        new CssMinimizerPlugin(),
      ],
    },
  };
};

改好之后,编译报如下错误 complier.plugin is not a function

 经查是webpack-cos-plugin插件报的错, Webpack5 发布后,各大主流 plugin 都已经相继适配webpack5新的plugin api, 而webpack-cos-plugin最新的版本是两年前的,近期没有做过维护,看完官网文档后,手动修复一下

compiler.hooks.emit.tap('WebpackQcloudCOSPlugin', (compilation) => {
    var files = _this.pickupAssetsFiles(compilation);
    log('' + green('\nCOS 上传开始......'));
    _this
        .uploadFiles(files, compilation)
        .then(function () {
        log('' + green('COS 上传完成\n'));
        })
        .catch(function (err) {
        log(red('COS 上传出错') + '::: ' + red(err.code) + '-' + red(err.name) + ': ' + red(err.message));
        _this.config.ignoreError || compilation.errors.push(err);
        });
});

然后在Linux机器上部署打包编译时,用修改之后的文件替换node_modules下的同名文件

\cp  -rf webpack/cos/index.js node_modules/webpack-cos-plugin/lib

运行打包命令,这次终于可以正常打包上传了,可是发现,打包之后的文件,页面中有些图片展示不出来,经查,未加载出来的图片,src的值是[object Module]


通过样式名查找,发现代码中凡是通过require给图片的src属性赋值的图片都加载不出来

<img src="require('assets/xxx.png')"/>

原因是url-loader最新版本默认情况下会把require引入的内容当做esModules去处理,而不是解析内容本身,所以要关闭默认解析方式。

module.exports = (env) => {
    return {
        // ...
        module: {
            rules: [
            {
                test: /\.(png|jpe?g|gif|ico|bmp)$/i,
                use: [
                {
                    loader: 'url-loader',
                    options: {
                        esModule: false, // 增加这一句
                        limit: 10000,
                        name: isDev ? 'image/[name][hash:8].[ext]' : 'image/[name].[contenthash:8].[ext]',
                    },
                },
                ],
            },
            ],
        },
    }
}

至此,大功告成。本地开发和生产打包所有的升级报错问题都已解决。可以愉快地享受webpack5带来全新打包体验。

 

后记:

1.webpack5已经不需要再引入url-loader,file-loader,raw-loader了,取而代之的是'asset',新旧写法如下:

        {
          test: /\.(png|jpe?g|gif|ico|bmp)$/i,
          use: [
            {
              loader: 'url-loader',
              options: {
                esModule: false,
                limit: 10000,
                name: isDev ? 'image/[name][hash:8].[ext]' : 'image/[name].[contenthash:8].[ext]',
              },
            },
          ],
        },
        {
          test: /\.(png|jpe?g|gif|ico|bmp)$/i,
          type: 'asset',
          parser: {
            dataUrlCondition: {
              // 转换成data-uri的条件
              maxSize: 10 * 1024, // 10kb
            },
          },
          generator: {
            filename: 'images/[hash][ext][query]', // 指定生成目录名称
          },
        },

 2. 用thread-loader替换happyoack

thread-loader 和 Happypack 构建时间基本没什么差别。不过 thread-loader 配置起来为简单。还有就是happypack的作者停止更新维护了。

module.exports = {
    module: {
        // babel-loader耗时比较长,所以我给它配置 thread-loader
        rules: [
            {
                test: /\.jsx?$/,
                use: ['thread-loader', 'babel-loader']
            }
        ]
    }
}

3. 不再需要cache-loader了

webpack4则需要借助cache-loaderhard-source-webpack-plugin来做缓存,一般会使用cache-loader将编译结构写入磁盘缓存,或者使用babel-loader?cacheDirectory=true,设置babel编译的结果写进磁盘缓存。

webpack5新增的cache属性,会默认开启磁盘缓存,默认将编译结果缓存在 node_modules/.cache/webpack目录下。

 4.推荐用esbuild-loader替换babel-loader

esbuild是由Go开发的用于打包压缩tsjs的工具,特点是打包速速很快,官方github介绍与webpackrollupParcel相比较要快几十甚至上百倍,但是esbuild目前还不能支持css, 并且没有插件机制,所以目前暂时替代不了webpack。但是有webpack中的loaderesbuild-loader。注意:esbuild-loader不支持装饰器语法,如果项目中使用了装饰器语法,那么就无法享受esbulid-loader快如闪电的打包速度。

const cpuNum = require("os").cpus().length;

const tsWorkerPool = {
  workers: 6,
  poolTimeout: Infinity,
};

module : {
  rules: [
    {
      test: /\.[jt]s$/,
      include: [resolve("../src")],
      exclude: [resolve("../node_modules")], // 屏蔽不需要处理的文件(文件夹)(可选)
      use: [
        {
          loader: "thread-loader",
          options: tsWorkerPool,
        },
        {
          loader: "esbuild-loader",

          options: {
            loader: "jsx", // Remove this if you're not using JSX
            target: "es2015", // Syntax to compile to (see options below for possible values)
          },
        },
        {
          loader: "esbuild-loader",
          options: {
            loader: "ts",
            target: "es2015",
            tsconfigRaw: require("../tsconfig.json"),
          },
        },
        // ...
      ],
    },
  ],
};

esbuild-loader: 相比babel-loader速度更快。

5.后面发现有一个npm工具包npm-check-updates,可以一键升级package.json里面的依赖包,解决升级webpack和webpack-cli后遇到各种各样的loaderplugin语法报错

npm install -g npm-check-updates
npm-check-updates // 检查package.json中哪些包有更新版本
ncu -u  // 更新package.json里面的工具包版本

 6.分离出webpack runtime代码

webpack在客户端运行时会首先加载webpack相关的代码,例如require函数等,这部分代码会随着每次修改业务代码后发生变化,原因是这里面会包含chunk id等容易变化的信息。如果不抽取出来将会被打包在vendor当中,导致vendor每次都要被用户重新加载。抽离的配置方式是:
module.exports = {
  //...
  optimization: {
    runtimeChunk: {
      name: 'runtime', // 会创建一个在所有生成 chunk 之间共享的运行时文件runtime
    },
  },
};

7. webpack module 优化配置-oneOf

每个不同类型的文件在loader转换时,会遍历module中rules中所有loader,即使已经匹配到某个规则了也会继续向下匹配。而如果将规则放在 oneOf 属性中,则一旦匹配到某个规则后,就停止匹配了。

配置如下:
rules:[
    {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: "eslint-loader",
    },
    {
        //  以下loader一种文件只会匹配一个 
        oneOf: [
            // 不能有两个配置处理同一种类型文件,如果有,另外一个规则要放到外面。
            {
                test: /\.js$/,
                exclude: /node_modules/,
                use: [
                    {
                        loader: "babel-loader",
                    },
                ],
            },
            {
                test: /\.css$/,
                use: [
                    "style-loader",
                    "css-loader",
                ],
            },
        ],
    },
]

 

参考文章

  • https://stackoverflow.com/questions/59070216/webpack-file-loader-outputs-object-module
  • https://stackoverflow.com/questions/64557638/how-to-polyfill-node-core-modules-in-webpack-5
  • https://webpack.js.org/api/cli/#env
  • https://webpack.docschina.org/blog/2020-10-10-webpack-5-release/
  • https://www.npmjs.com/package/webpack-cos-plugin
  • https://blog.csdn.net/qq_36741436/article/details/78732201
  • https://webpack.js.org/api/plugins/#plugin-types
  • https://juejin.cn/post/6977183266986000414

 

posted @ 2021-05-13 22:59  孤舟蓑翁  阅读(5847)  评论(3编辑  收藏  举报