玩轉 webpack,使你的打包速度提高 90%

前言

webpack 打包優化並無什麼固定的模式,通常咱們常見的優化就是拆包、分塊、壓縮等,並非對每個項目都適用,針對於特定項目,須要不斷調試不斷優化。css

對於 webpack4,建議從零開始配置,在項目初期,使用 webpack4 默認的配置。前端

接下來,本篇文章會列出全部適用於 webpack 優化打包速度的技術方案,並給出相應的限制,請在實際項目中分狀況使用。若有任何疑問,請聯繫瓶子君。node

1、分析打包速度

優化 webpack 構建速度的第一步是知道將精力集中在哪裏。咱們能夠經過 speed-measure-webpack-plugin 測量你的 webpack 構建期間各個階段花費的時間:react

// 分析打包時間
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();
// ...
module.exports = smp.wrap(prodWebpackConfig)
複製代碼

特定的項目,都有本身特定的性能構建瓶頸,下面咱們對打包的每個環節進行優化。jquery

2、分析影響打包速度環節

在「窺探原理:手寫一個 JavaScript 打包器」中,咱們已經介紹過,打包就是從入口文件開始將全部的依賴模塊打包到一個文件中的過程,固然,在打包過程當中涉及各類編譯、優化過程。webpack

打包過程當中,常見影響構建速度的地方有哪些喃?git

1. 開始打包,咱們須要獲取全部的依賴模塊

搜索全部的依賴項,這須要佔用必定的時間,即搜索時間,那麼咱們就肯定了:github

咱們須要優化的第一個時間就是搜索時間。web

2. 解析全部的依賴模塊(解析成瀏覽器可運行的代碼)

webpack 根據咱們配置的 loader 解析相應的文件。平常開發中咱們須要使用 loader 對 js ,css ,圖片,字體等文件作轉換操做,而且轉換的文件數據量也是很是大。因爲 js 單線程的特性使得這些轉換操做不能併發處理文件,而是須要一個個文件進行處理。正則表達式

咱們須要優化的第二個時間就是解析時間。

3. 將全部的依賴模塊打包到一個文件

將全部解析完成的代碼,打包到一個文件中,爲了使瀏覽器加載的包更新(減少白屏時間),因此 webpack 會對代碼進行優化。

JS 壓縮是發佈編譯的最後階段,一般 webpack 須要卡好一會,這是由於壓縮 JS 須要先將代碼解析成 AST 語法樹,而後須要根據複雜的規則去分析和處理 AST,最後將 AST 還原成 JS,這個過程涉及到大量計算,所以比較耗時,打包就容易卡住。

咱們須要優化的第三個時間就是壓縮時間。

4. 二次打包

當更改項目中一個小小的文件時,咱們須要從新打包,全部的文件都必需要從新打包,須要花費同初次打包相同的時間,但項目中大部分文件都沒有變動,尤爲是第三方庫。

咱們須要優化的第四個時間就是二次打包時間。

3、 優化解析時間 - 開啓多進程打包

運行在 Node.js 之上的 webpack 是單線程模式的,也就是說,webpack 打包只能逐個文件處理,當 webpack 須要打包大量文件時,打包時間就會比較漫長。

1. 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)中運行。

例如:

module.exports = {
  // ...
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        // 建立一個 js worker 池
        use: [ 
          'thread-loader',
          'babel-loader'
        ] 
      },
      {
        test: /\.s?css$/,
        exclude: /node_modules/,
        // 建立一個 css worker 池
        use: [
          'style-loader',
          'thread-loader',
          {
            loader: 'css-loader',
            options: {
              modules: true,
              localIdentName: '[name]__[local]--[hash:base64:5]',
              importLoaders: 1
            }
          },
          'postcss-loader'
        ]
      }
      // ...
    ]
    // ...
  }
  // ...
}
複製代碼

注意:thread-loader 放在了 style-loader 以後,這是由於 thread-loader 後的 loader 無法存取文件也無法獲取 webpack 的選項設置。

官方上說每一個 worker 大概都要花費 600ms ,因此官方爲了防止啓動 worker 時的高延遲,提供了對 worker 池的優化:預熱

// ...
const threadLoader = require('thread-loader');

const jsWorkerPool = {
  // options
  
  // 產生的 worker 的數量,默認是 (cpu 核心數 - 1)
  // 當 require('os').cpus() 是 undefined 時,則爲 1
  workers: 2,
  
  // 閒置時定時刪除 worker 進程
  // 默認爲 500ms
  // 能夠設置爲無窮大, 這樣在監視模式(--watch)下能夠保持 worker 持續存在
  poolTimeout: 2000
};

const cssWorkerPool = {
  // 一個 worker 進程中並行執行工做的數量
  // 默認爲 20
  workerParallelJobs: 2,
  poolTimeout: 2000
};

threadLoader.warmup(jsWorkerPool, ['babel-loader']);
threadLoader.warmup(cssWorkerPool, ['css-loader', 'postcss-loader']);


module.exports = {
  // ...
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'thread-loader',
            options: jsWorkerPool
          },
          'babel-loader'
        ]
      },
      {
        test: /\.s?css$/,
        exclude: /node_modules/,
        use: [
          'style-loader',
          {
            loader: 'thread-loader',
            options: cssWorkerPool
          },
          {
            loader: 'css-loader',
            options: {
              modules: true,
              localIdentName: '[name]__[local]--[hash:base64:5]',
              importLoaders: 1
            }
          },
          'postcss-loader'
        ]
      }
      // ...
    ]
    // ...
  }
  // ...
}
複製代碼

注意:請僅在耗時的 loader 上使用。

2. HappyPack

在webpack構建過程當中,實際上耗費時間大多數用在 loader 解析轉換以及代碼的壓縮中,HappyPack 可利用多進程對文件進行打包(默認cpu核數-1),對多核cpu利用率更高。HappyPack 可讓 Webpack 同一時間處理多個任務,發揮多核 CPU 的能力,將任務分解給多個子進程去併發的執行,子進程處理完後,再把結果發送給主進程。

happypack 的處理思路是將原有的 webpack 對 loader 的執行過程從單一進程的形式擴展多進程模式,本來的流程保持不變。使用 HappyPack 也有一些限制,它只兼容部分主流的 loader,具體能夠查看官方給出的 兼容性列表

注意:Ahmad Amireh 推薦使用 thread-loader,並宣佈將再也不繼續維護 happypack,因此不推薦使用它

const path = require('path')
const webpack = require("webpack");
const HappyPack = require('happypack'); // 多進程loader
// node 提供的系統操做模塊
const os = require('os');
// 構造出共享進程池,根據系統的內核數量,指定進程池個數,也能夠其餘數量
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });
const createHappyPlugin = (id, loaders) => new HappyPack({
  // 用惟一的標識符 id 來表明當前的 HappyPack 是用來處理一類特定的文件
  id: id,
  // 如何處理 .js 文件,用法和 Loader 配置中同樣
  loaders: loaders,
  // 其它配置項(可選)
  // 表明共享進程池,即多個 HappyPack 實例都使用同一個共享進程池中的子進程去處理任務,以防止資源佔用過多
  threadPool: happyThreadPool,
  // 是否容許 HappyPack 輸出日誌,默認是 true
  verbose: true
  // threads:表明開啓幾個子進程去處理這一類型的文件,默認是3個,類型必須是整數
});

const clientWebpackConfig = {
  // ...
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        // 把對 .js .jsx 文件的處理轉交給 id 爲 happy-babel 的 HappyPack 實例
        use: ["happypack/loader?id=happy-babel"],
        // 排除 node_modules 目錄下的文件
        // node_modules 目錄下的文件都是採用的 ES5 語法,不必再經過 Babel 去轉換
        exclude: /node_modules/,
      }
    ]
  },
  // ...
  plugins: [
    createHappyPlugin('happy-babel', [{
      loader: 'babel-loader',
      options: {
        presets: ['@babel/preset-env', "@babel/preset-react"],
        plugins: [
          ["import", { "libraryName": "antd", "style": true }],
          ['@babel/plugin-proposal-class-properties',{loose:true}]
        ],
        cacheDirectory: true,
        // Save disk space when time isn't as important
        cacheCompression: true,
        compact: true,
      }
    }]),
    // ...
  ]
}
複製代碼

注意,當項目較小時,多進程打包反而會使打包速度變慢。

4、合理利用緩存(縮短連續構建時間,增長初始構建時間)

使用 webpack 緩存的方法有幾種,例如使用 cache-loaderHardSourceWebpackPluginbabel-loadercacheDirectory 標誌。 全部這些緩存方法都有啓動的開銷。 從新運行期間在本地節省的時間很大,可是初始(冷)運行實際上會更慢。

若是你的項目生產版本每次都必須進行初始構建的話,緩存會增長構建時間,減慢你的速度。若是不是,那它們就會大大縮減你二次構建的時間。

1. cache-loader

cache-loader 和 thread-loader 同樣,使用起來也很簡單,僅僅須要在一些性能開銷較大的 loader 以前添加此 loader,以將結果緩存到磁盤裏,顯著提高二次構建速度。

module.exports = {
  module: {
    rules: [
      {
        test: /\.ext$/,
        use: ['cache-loader', ...loaders],
        include: path.resolve('src'),
      },
    ],
  },
};
複製代碼

⚠️ 請注意,保存和讀取這些緩存文件會有一些時間開銷,因此請只對性能開銷較大的 loader 使用此 loader。

2. HardSourceWebpackPlugin

  • 第一次構建將花費正常的時間
  • 第二次構建將顯着加快(大概提高90%的構建速度)。
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin')
const clientWebpackConfig = {
  // ...
  plugins: [
    new HardSourceWebpackPlugin({
      // cacheDirectory是在高速緩存寫入。默認狀況下,將緩存存儲在node_modules下的目錄中
      // 'node_modules/.cache/hard-source/[confighash]'
      cacheDirectory: path.join(__dirname, './lib/.cache/hard-source/[confighash]'),
      // configHash在啓動webpack實例時轉換webpack配置,
      // 並用於cacheDirectory爲不一樣的webpack配置構建不一樣的緩存
      configHash: function(webpackConfig) {
        // node-object-hash on npm can be used to build this.
        return require('node-object-hash')({sort: false}).hash(webpackConfig);
      },
      // 當加載器、插件、其餘構建時腳本或其餘動態依賴項發生更改時,
      // hard-source須要替換緩存以確保輸出正確。
      // environmentHash被用來肯定這一點。若是散列與先前的構建不一樣,則將使用新的緩存
      environmentHash: {
        root: process.cwd(),
        directories: [],
        files: ['package-lock.json', 'yarn.lock'],
      },
      // An object. 控制來源
      info: {
        // 'none' or 'test'.
        mode: 'none',
        // 'debug', 'log', 'info', 'warn', or 'error'.
        level: 'debug',
      },
      // Clean up large, old caches automatically.
      cachePrune: {
        // Caches younger than `maxAge` are not considered for deletion. They must
        // be at least this (default: 2 days) old in milliseconds.
        maxAge: 2 * 24 * 60 * 60 * 1000,
        // All caches together must be larger than `sizeThreshold` before any
        // caches will be deleted. Together they must be at least this
        // (default: 50 MB) big in bytes.
        sizeThreshold: 50 * 1024 * 1024
      },
    }),
    new HardSourceWebpackPlugin.ExcludeModulePlugin([
      {
        test: /.*\.DS_Store/
      }
    ]),
  ]
}
複製代碼

5、優化壓縮時間

1. webpack3

webpack3 啓動打包時加上 --optimize-minimize ,這樣 Webpack 會自動爲你注入一個帶有默認配置的 UglifyJSPlugin 。

或:

module.exports = {
    optimization: {
        minimize: true,
    },
}
複製代碼

壓縮 JavaScript 代碼須要先把代碼解析成用 Object 抽象表示的 AST 語法樹,再去應用各類規則分析和處理 AST,致使這個過程計算量巨大,耗時很是多。但 UglifyJsPlugin 是單線程,因此咱們可使用 ParallelUglifyPlugin

ParallelUglifyPlugin 插件實現了多進程壓縮,ParallelUglifyPlugin 會開啓多個子進程,把對多個文件的壓縮工做分配給多個子進程去完成,每一個子進程其實仍是經過 UglifyJS 去壓縮代碼,可是變成了並行執行。 因此 ParallelUglifyPlugin 能更快的完成對多個文件的壓縮工做。

2. webpack4

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

module.exports = {
  optimization: {
    minimizer: [
      new TerserPlugin({
        parallel: true,
      }),
    ],
  },
};
複製代碼

能夠顯著加快構建速度,所以強烈推薦開啓多進程

6、優化搜索時間- 縮小文件搜索範圍 減少沒必要要的編譯工做

webpack 打包時,會從配置的 entry 觸發,解析入口文件的導入語句,再遞歸的解析,在遇到導入語句時 webpack 會作兩件事情:

  • 根據導入語句去尋找對應的要導入的文件。例如 require('react') 導入語句對應的文件是 ./node_modules/react/react.jsrequire('./util') 對應的文件是 ./util.js
  • 根據找到的要導入文件的後綴,使用配置中的 Loader 去處理文件。例如使用 ES6 開發的 JavaScript 文件須要使用 babel-loader 去處理。

以上兩件事情雖然對於處理一個文件很是快,可是當項目大了之後文件量會變的很是多,這時候構建速度慢的問題就會暴露出來。 雖然以上兩件事情沒法避免,但須要儘可能減小以上兩件事情的發生,以提升速度。

接下來一一介紹能夠優化它們的途徑。

1. 優化 loader 配置

使用 Loader 時能夠經過 test 、 include 、 exclude 三個配置項來命中 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 文件裏,以下:

{
  "jsnext:main": "es/index.js",// 採用 ES6 語法的代碼入口文件
  "main": "lib/index.js" // 採用 ES5 語法的代碼入口文件
}
複製代碼

webpack 會根據 mainFields 的配置去決定優先採用那份代碼,mainFields 默認以下:

mainFields: ['browser', 'main']
複製代碼

webpack 會按照數組裏的順序去 package.json 文件裏尋找,只會使用找到的第一個。

假如你想優先採用 ES6 的那份代碼,能夠這樣配置:

mainFields: ['jsnext:main', 'browser', 'main']
複製代碼

6. 優化 module.noParse 配置

module.noParse 配置項可讓 Webpack 忽略對部分沒采用模塊化的文件的遞歸解析處理,這樣作的好處是能提升構建性能。 緣由是一些庫,例如 jQuery 、ChartJS, 它們龐大又沒有采用模塊化標準,讓 Webpack 去解析這些文件耗時又沒有意義。

7. 詳細配置

// 編譯代碼的基礎配置
module.exports = {
  // ...
  module: {
    // 項目中使用的 jquery 並無採用模塊化標準,webpack 忽略它
    noParse: /jquery/,
    rules: [
      {
        // 這裏編譯 js、jsx
        // 注意:若是項目源碼中沒有 jsx 文件就不要寫 /\.jsx?$/,提高正則表達式性能
        test: /\.(js|jsx)$/,
        // babel-loader 支持緩存轉換出的結果,經過 cacheDirectory 選項開啓
        use: ['babel-loader?cacheDirectory'],
        // 排除 node_modules 目錄下的文件
        // node_modules 目錄下的文件都是採用的 ES5 語法,不必再經過 Babel 去轉換
        exclude: /node_modules/,
      },
    ]
  },
  resolve: {
    // 設置模塊導入規則,import/require時會直接在這些目錄找文件
    // 能夠指明存放第三方模塊的絕對路徑,以減小尋找
    modules: [
      path.resolve(`${project}/client/components`), 
      path.resolve('h5_commonr/components'), 
      'node_modules'
    ],
    // import導入時省略後綴
    // 注意:儘量的減小後綴嘗試的可能性
    extensions: ['.js', '.jsx', '.react.js', '.css', '.json'],
    // import導入時別名,減小耗時的遞歸解析操做
    alias: {
      '@compontents': path.resolve(`${project}/compontents`),
    }
  },
};
複製代碼

以上就是全部和縮小文件搜索範圍相關的構建性能優化了,在根據本身項目的須要去按照以上方法改造後,你的構建速度必定會有所提高。

6、往期 webpack 系列

五種可視化方案分析 webpack 打包性能瓶頸

webpack 最佳配置指北

窺探原理:手寫一個 JavaScript 打包器

是時候拋棄Postman了,試試 VS Code 自帶神器插件👏👏👏

若是以爲不錯,就點個贊吧!👍👍👍

想看往期更過系列文章,點擊前往 github 博客主頁

7、走在最後

  1. ❤️玩得開心,不斷學習,並始終保持編碼。👨💻

  2. 若有任何問題或更獨特的看法,歡迎評論或直接聯繫瓶子君(掃碼關注公衆號回覆 123 便可)!👀👇

  3. 👇歡迎關注:前端瓶子君,每日更新!👇

相關文章
相關標籤/搜索