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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM