1. 程式人生 > 實用技巧 >Webpack 效能優化

Webpack 效能優化

減少 Webpack 打包時間

優化 Loader

對於 Loader 來說,影響打包效率首當其衝必屬 Babel 了。因為 Babel 會將程式碼轉為字串生成 AST,然後對 AST 繼續進行轉變最後再生成新的程式碼,專案越大,轉換程式碼越多,效率就越低。當然了,我們是有辦法優化的。

首先我們可以優化 Loader 的檔案搜尋範圍

module.exports = {
  module: {
    rules: [
      {
        // js 檔案才使用 babel
        test: /\.js$/,
        loader: 'babel-loader',
        // 只在 src 資料夾下查詢
        include: [resolve('src')],
        // 不會去查詢的路徑
        exclude: /node_modules/
      }
    ]
  }
}

對於 Babel 來說,我們肯定是希望只作用在 JS 程式碼上的,然後node_modules中使用的程式碼都是編譯過的,所以我們也完全沒有必要再去處理一遍。

當然這樣做還不夠,我們還可以將 Babel 編譯過的檔案快取起來,下次只需要編譯更改過的程式碼檔案即可,這樣可以大幅度加快打包時間

loader: 'babel-loader?cacheDirectory=true'

HappyPack

受限於 Node 是單執行緒執行的,所以 Webpack 在打包的過程中也是單執行緒的,特別是在執行 Loader 的時候,長時間編譯的任務很多,這樣就會導致等待的情況。

HappyPack 可以將 Loader 的同步執行轉換為並行的,這樣就能充分利用系統資源來加快打包效率了

module: {
  loaders: [
    {
      test: /\.js$/,
      include: [resolve('src')],
      exclude: /node_modules/,
      // id 後面的內容對應下面
      loader: 'happypack/loader?id=happybabel'
    }
  ]
},
plugins: [
  new HappyPack({
    id: 'happybabel',
    loaders: ['babel-loader?cacheDirectory'],
    // 開啟 4 個執行緒
    threads: 4
  })
]

DllPlugin

DllPlugin 可以將特定的類庫提前打包然後引入。這種方式可以極大的減少打包類庫的次數,只有當類庫更新版本才有需要重新打包,並且也實現了將公共程式碼抽離成單獨檔案的優化方案。

接下來我們就來學習如何使用 DllPlugin

// 單獨配置在一個檔案中
// webpack.dll.conf.js
const path = require('path')
const webpack = require('webpack')
module.exports = {
  entry: {
    // 想統一打包的類庫
    vendor: ['react']
  },
  output: {
    path: path.join(__dirname, 'dist'),
    filename: '[name].dll.js',
    library: '[name]-[hash]'
  },
  plugins: [
    new webpack.DllPlugin({
      // name 必須和 output.library 一致
      name: '[name]-[hash]',
      // 該屬性需要與 DllReferencePlugin 中一致
      context: __dirname,
      path: path.join(__dirname, 'dist', '[name]-manifest.json')
    })
  ]
}

然後我們需要執行這個配置檔案生成依賴檔案,接下來我們需要使用DllReferencePlugin將依賴檔案引入專案中

// webpack.conf.js
module.exports = {
  // ...省略其他配置
  plugins: [
    new webpack.DllReferencePlugin({
      context: __dirname,
      // manifest 就是之前打包出來的 json 檔案
      manifest: require('./dist/vendor-manifest.json'),
    })
  ]
}

程式碼壓縮

在 Webpack3 中,我們一般使用UglifyJS來壓縮程式碼,但是這個是單執行緒執行的,為了加快效率,我們可以使用webpack-parallel-uglify-plugin來並行執行UglifyJS,從而提高效率。

在 Webpack4 中,我們就不需要以上這些操作了,只需要將mode設定為production就可以預設開啟以上功能。程式碼壓縮也是我們必做的效能優化方案,當然我們不止可以壓縮 JS 程式碼,還可以壓縮 HTML、CSS 程式碼,並且在壓縮 JS 程式碼的過程中,我們還可以通過配置實現比如刪除console.log這類程式碼的功能。

一些小的優化點

我們還可以通過一些小的優化點來加快打包速度

  • resolve.extensions:用來表明檔案字尾列表,預設查詢順序是['.js', '.json'],如果你的匯入檔案沒有新增字尾就會按照這個順序查詢檔案。我們應該儘可能減少字尾列表長度,然後將出現頻率高的字尾排在前面
  • resolve.alias:可以通過別名的方式來對映一個路徑,能讓 Webpack 更快找到路徑
  • module.noParse:如果你確定一個檔案下沒有其他依賴,就可以使用該屬性讓 Webpack 不掃描該檔案,這種方式對於大型的類庫很有幫助

減少 Webpack 打包後的檔案體積

注意:該內容也屬於效能優化領域。

按需載入

想必大家在開發 SPA 專案的時候,專案中都會存在十幾甚至更多的路由頁面。如果我們將這些頁面全部打包進一個 JS 檔案的話,雖然將多個請求合併了,但是同樣也載入了很多並不需要的程式碼,耗費了更長的時間。那麼為了首頁能更快地呈現給使用者,我們肯定是希望首頁能載入的檔案體積越小越好,這時候我們就可以使用按需載入,將每個路由頁面單獨打包為一個檔案。當然不僅僅路由可以按需載入,對於loadash這種大型類庫同樣可以使用這個功能。

按需載入的程式碼實現這裡就不詳細展開了,因為鑑於用的框架不同,實現起來都是不一樣的。當然了,雖然他們的用法可能不同,但是底層的機制都是一樣的。都是當使用的時候再去下載對應檔案,返回一個Promise,當Promise成功以後去執行回撥。

Scope Hoisting

Scope Hoisting 會分析出模組之間的依賴關係,儘可能的把打包出來的模組合併到一個函式中去。

比如我們希望打包兩個檔案

// test.js
export const a = 1
// index.js
import { a } from './test.js'

對於這種情況,我們打包出來的程式碼會類似這樣

[
  /* 0 */
  function (module, exports, require) {
    //...
  },
  /* 1 */
  function (module, exports, require) {
    //...
  }
]

但是如果我們使用 Scope Hoisting 的話,程式碼就會盡可能的合併到一個函式中去,也就變成了這樣的類似程式碼

[
  /* 0 */
  function (module, exports, require) {
    //...
  }
]

這樣的打包方式生成的程式碼明顯比之前的少多了。如果在 Webpack4 中你希望開啟這個功能,只需要啟用optimization.concatenateModules就可以了。

module.exports = {
  optimization: {
    concatenateModules: true
  }
}

Tree Shaking

Tree Shaking 可以實現刪除專案中未被引用的程式碼,比如

// test.js
export const a = 1
export const b = 2
// index.js
import { a } from './test.js'

對於以上情況,test檔案中的變數b如果沒有在專案中使用到的話,就不會被打包到檔案中。

如果你使用 Webpack 4 的話,開啟生產環境就會自動啟動這個優化功能