Skip to content
⚠️ This article was written in 2019. Some content may be outdated.

Webpack DllPlugin 加速構建

Webpack 專案一大痛點就是構建速度慢。一箇中型專案,node_modules 裡幾百個包,每次 npm run build 都要等好幾分鐘。DllPlugin 是 Webpack 官方提供的構建最佳化方案之一,核心思路是把不常變化的依賴(如 React、Vue、lodash 等)提前打包成一個獨立的 DLL 檔案,主構建時直接引用,不再重複編譯。

DllPlugin 原理

第一次構建(生成 DLL):
  react, react-dom, lodash, axios
  -> 打包成 vendor.dll.js + vendor.manifest.json
  -> 耗時:一次性開銷

後續構建(主構建):
  讀取 manifest.json -> 知道 DLL 中已經包含哪些模組
  跳過這些模組的解析和編譯 -> 只編譯業務程式碼
  -> 耗時:大幅減少

完整配置

第一步:建立 DLL 構建配置

javascript
// webpack.dll.config.js
const path = require('path')
const webpack = require('webpack')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')

module.exports = {
  mode: 'production',
  entry: {
    // 把不常變化的依賴打包進 DLL
    vendor: [
      'react',
      'react-dom',
      'react-router-dom',
      'axios',
      'lodash',
      'moment'
    ]
  },
  output: {
    path: path.resolve(__dirname, 'dll'),
    filename: '[name].dll.js',
    library: '[name]_library'  // 暴露為全域性變數,供 DllReferencePlugin 引用
  },
  plugins: [
    new CleanWebpackPlugin(),
    new webpack.DllPlugin({
      name: '[name]_library',
      path: path.resolve(__dirname, 'dll/[name].manifest.json')
      // manifest.json 記錄了模組 ID 和檔案路徑的對映關係
    })
  ]
}

第二步:在主構建中引用 DLL

javascript
// webpack.config.js
const path = require('path')
const webpack = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  mode: 'production',
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'js/[name].[contenthash:8].js'
  },
  plugins: [
    // 引用 DLL,告訴 Webpack 這些模組已經打包好了,不需要重複處理
    new webpack.DllReferencePlugin({
      manifest: require('./dll/vendor.manifest.json')
    }),

    new HtmlWebpackPlugin({
      template: './public/index.html',
      // 注意:需要手動把 dll.js 檔案引入到 HTML 中
      // 可以用 AddAssetHtmlPlugin 自動注入
    })
  ]
}

第三步:自動注入 DLL 到 HTML

手動在 HTML 模板中引入 vendor.dll.js 太麻煩了。用 add-asset-html-webpack-plugin 自動處理:

bash
npm install add-asset-html-webpack-plugin --save-dev
javascript
// webpack.config.js
const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin')

module.exports = {
  plugins: [
    new webpack.DllReferencePlugin({
      manifest: require('./dll/vendor.manifest.json')
    }),

    // 自動把 DLL 檔案注入到 HTML 中
    new AddAssetHtmlPlugin({
      filepath: path.resolve(__dirname, 'dll/*.dll.js'),
      outputPath: 'dll',
      publicPath: '/dll'
    }),

    new HtmlWebpackPlugin({
      template: './public/index.html'
    })
  ]
}

第四步:配置 npm scripts

json
{
  "scripts": {
    "dll": "webpack --config webpack.dll.config.js",
    "build": "npm run dll && webpack --config webpack.config.js",
    "build:dll": "npm run dll",
    "build:main": "webpack --config webpack.config.js"
  }
}
  • npm run dll:單獨生成 DLL(只在依賴變化時需要執行)
  • npm run build:先生成 DLL 再構建
  • npm run build:main:跳過 DLL 生成,直接主構建(日常開發用這個)

多個 DLL 分包

如果專案很大,可以把 DLL 拆成多個:

javascript
// webpack.dll.config.js
module.exports = {
  entry: {
    // React 生態
    react: ['react', 'react-dom', 'react-router-dom', 'redux', 'react-redux'],
    // 工具庫
    utils: ['axios', 'lodash', 'moment', 'classnames'],
    // UI 元件庫
    ui: ['antd']
  },
  output: {
    path: path.resolve(__dirname, 'dll'),
    filename: '[name].dll.js',
    library: '[name]_library'
  },
  plugins: [
    new webpack.DllPlugin({
      name: '[name]_library',
      path: path.resolve(__dirname, 'dll/[name].manifest.json')
    })
  ]
}

主構建中引用多個 DLL:

javascript
// webpack.config.js
plugins: [
  new webpack.DllReferencePlugin({
    manifest: require('./dll/react.manifest.json')
  }),
  new webpack.DllReferencePlugin({
    manifest: require('./dll/utils.manifest.json')
  }),
  new webpack.DllReferencePlugin({
    manifest: require('./dll/ui.manifest.json')
  }),
  new AddAssetHtmlPlugin({
    filepath: path.resolve(__dirname, 'dll/*.dll.js'),
    outputPath: 'dll',
    publicPath: '/dll'
  })
]

與 HardSourceWebpackPlugin 對比

2019 年另一個流行的構建加速方案是 HardSourceWebpackPlugin,它通過快取模組的編譯結果來加速。

| 特性 | DllPlugin | HardSourceWebpackPlugin | | ------|-----------|------------------------| | 原理 | 預打包不變的依賴 | 快取所有模組的編譯結果 | | 首次構建 | 需要先構建 DLL | 沒有加速效果 | | 後續構建 | DLL 中的模組跳過編譯 | 所有模組從快取恢復 | | 依賴變化 | 需要重新構建 DLL | 自動增量更新 | | 配置複雜度 | 需要單獨的 DLL 配置檔案 | 一行配置即可 | | 適用場景 | 依賴穩定的大型專案 | 任何專案 |

javascript
// HardSourceWebpackPlugin 配置(非常簡單)
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin')

module.exports = {
  plugins: [
    new HardSourceWebpackPlugin()
    // 就這一行,其餘交給它自動處理
  ]
}

能不能同時用?

可以。DllPlugin 減少首次構建時間(跳過依賴編譯),HardSourceWebpackPlugin 加速後續構建(快取編譯結果)。兩者互補:

javascript
plugins: [
  new webpack.DllReferencePlugin({
    manifest: require('./dll/vendor.manifest.json')
  }),
  new HardSourceWebpackPlugin(),
  new AddAssetHtmlPlugin(/* ... */)
]

實際效果

在一箇中型 React 專案(約 150 個模組)上測試:

場景不最佳化只用 DllPlugin只用 HardSource兩者都用
冷啟動構建45s35s45s35s
二次構建45s35s8s6s
依賴更新後45s45s15s15s

可以看到:

  • DllPlugin 主要減少冷啟動時間(省去依賴編譯)
  • HardSourceWebpackPlugin 主要加速二次構建
  • 兩者結合效果最好

踩坑記錄

坑 1:DLL 構建後忘記重新構建

添加了新依賴後,需要重新執行 npm run dll,否則主構建會找不到新模組。

解決:在 package.jsonpostinstall 中自動重建 DLL:

json
{
  "scripts": {
    "postinstall": "npm run dll"
  }
}

但這樣每次 npm install 都會重建 DLL,可能比較慢。更好的方式是用一個檢查指令碼:

javascript
// scripts/check-dll.js
const fs = require('fs')
const path = require('path')
const pkg = require('../package.json')

const dllEntry = require('../webpack.dll.config.js').entry.vendor
const manifestPath = path.resolve(__dirname, '../dll/vendor.manifest.json')

if (!fs.existsSync(manifestPath)) {
  console.log('DLL manifest 不存在,需要構建 DLL')
  process.exit(1) // 觸發重建
}

console.log('DLL 已存在,跳過')

坑 2:DllPlugin 和 scope hoisting 衝突

ModuleConcatenationPlugin(scope hoisting)可能會導致 DLL 中的模組 ID 變化,與 manifest 不匹配。

解決:DLL 構建配置中關閉 scope hoisting:

javascript
// webpack.dll.config.js
module.exports = {
  optimization: {
    concatenateModules: false
  }
}

坑 3:DLL 檔案版本管理

DLL 檔案體積通常較大(幾十 MB),是否提交到 Git?

建議:

  • 小團隊、CI/CD 完善:不提交,CI 中構建
  • 沒有 CI:提交到 Git,讓團隊共享
ini
# 如果不提交
dll/

坑 4:開發模式下的 sourcemap

DLL 檔案在開發時也需要 sourcemap,否則除錯時找不到原始碼:

javascript
// webpack.dll.config.js(開發用)
module.exports = {
  mode: 'development',
  devtool: 'source-map',
  // ...
}

小結

  • DllPlugin 的核心原理:把不常變化的依賴預打包,主構建時跳過這些模組的編譯
  • 配置三步走:建立 DLL 構建配置 -> 主構建用 DllReferencePlugin 引用 -> 用 AddAssetHtmlPlugin 注入 HTML
  • HardSourceWebpackPlugin 通過快取編譯結果加速,和 DllPlugin 互補
  • 依賴變化後需要重新構建 DLL,可以在 postinstall 中自動處理
  • DLL 檔案體積大,需要決定是否提交到版本控制
  • 現代 Webpack 5 中 DllPlugin 的使用場景減少(因為有更好的持久化快取方案),但在 Webpack 4 時代是構建加速的重要手段

MIT Licensed