webpack持久化緩存優化策略

作者:淼森

問題背景

webpack 是目前最流行的前端依賴打包工具,日常項目開發當中利用webpack在編譯打包生成靜態資源文件時,通過相應的配置以及引入某些插件,我們給靜態資源js、css等添加chunkhash,從而可以根據代碼文件的變動,自動生成相應的文件chunkhash,最終可以充分利用瀏覽器的靜態資源緩存能力。

通常在我們會使用CommonsChunkPlugin來將所有依賴的第三方包打包到一個名為vender的chunk中。與此同時,為了避免每次更改項目代碼時導致vender chunk的chunkHash改變,我們還會單獨生成一個manifest chunk。

但在實際項目當中經常遇到一些chunkhash失效的問題,即只在業務模塊中新增了一個新的依賴模塊,重新編譯打包,就會發現導致抽離出的公共基礎庫模塊 vendor.js的chunkhash發生變化,而這絕對不是我們所期望的情況,因為從模塊依賴關係上來看業務應用模塊中的新增模塊與vendor.js並沒有直接的依賴關係。

而這就要從webpack的持久化緩存說起。。。

持久化緩存

持久緩存,也就是說在相關代碼內容沒有變化的時候,儘可能的使瀏覽器利用緩存而不是發送靜態資源請求。

常用的方法:

  • 將很少變動代碼提取到單獨文件,使得這部分代碼幾乎總是被緩存,從而提高整體靜態資源緩存利用率
  • 使用數據摘要要演算法對文件求摘要信息,摘要信息與文件內容一一對應,將摘要信息作為文件名(的一部分)或者版本號,同時設置最大緩存失效期

webpack實現持久化緩存

代碼分離 code splitting

webpack基礎配置

  • 提取webpack運行庫第三方基礎庫代碼

// 將基礎庫代碼提取到單獨文件中 new webpack.optimize.CommonsChunkPlugin({ name: vendor, minChunks: function (module, count) { return ( module.resource && /.js$/.test(module.resource) && module.resource.indexOf( path.join(__dirname, ../node_modules) ) === 0 ) } }), new webpack.optimize.CommonsChunkPlugin({ name: manifest, chunks: [vendor] }),

  • 應用入口entry chunk提取

module.exports = { entry: { main: ./src/entry/newPage.jsx, }, output: { path: ./dist, filename: [name].js },

  • css文件提取

module: { rules: [{ test: /.css?$/, use: ExtractTextPlugin.extract({ fallback: style-loader, use: css-loader }) }]}

webpack文件hash

文件的hash指紋通常作為前端靜態資源實現持久化緩存的方案之一,Webpack提供了兩個配置項可供使用:hash和chunkhash。那麼兩者有何區別呢?

區分 [hash][chunkhash]

  • hash代表的是compilation的hash值
    • compilation對象代表某個版本的資源對應的編譯進程,任何一個文件改動後就會被重新創建,然後webpack計算新的compilation的hash值
    • 所有的文件名都會使用相同的hash指紋
  • chunkhash代表的是chunk的hash值
    • 根據具體chunk模塊文件的內容計算所得的hash值,所以某個文件的改動只會影響它本身的hash指紋,不會影響其他文件

webpack配置使用 [chunkhash]

  • output文件名js chunkhash配置

output: { path: ./dist/, //為了減少提交文件數,採用 ?_v=[chunkhash:8]的文件命名規則 filename: utils.assetsPath(js/[name].js?_v=[chunkhash:8]), chunkFilename: utils.assetsPath(js/[id].chunk.js?_v=[chunkhash:8]), // filename: utils.assetsPath(js/[name].[chunkhash:8].js), // chunkFilename: utils.assetsPath(js/[id].[chunkhash:8].js) }

filename (官方文檔)對應於entry chunk生成的文件名。單個入口filename 會是一個靜態名稱,多個入口起點使用[name]來使用對應的配置名。

chunkFilename (官方文檔)對應於非入口(non-entry) chunk 文件,需要被單獨打包出來的文件命名配置。比如按需載入(非同步)模塊的時候,這樣的文件是沒有被列在entry中的使用CommonJS的方式非同步載入模塊

  • 提取css文件chunkhash配置

const ExtractTextPlugin = require(extract-text-webpack-plugin);new ExtractTextPlugin({ filename: utils.assetsPath(css/[name].css?_v=[contenthash:8])}),

不穩定的chunkhash

不過,只是計算 chunk MD5 摘要並修改 chunk 資源文件名是不夠的。Chunk 的生成還涉及到依賴解析和模塊 ID 分配,這是無法穩定實質上沒有變化的 chunk 文件的 chunkhash 變動問題的本源

對於複雜項目的構建,由於模塊間互相依賴,可能只改動了一個小模塊,但在構建後,會發現所有與之直接或間接相關的 chunk 及其 chunkhash 都被更新了。

模塊依賴導致chunk更新示例

以赤兔平台的代碼為例:

  • app.jsx 是平台的業務模塊入口文件(entry chunk)所依賴的模塊

import React from react;import {connect} from react-redux;import EditModule from ./EditModule;import BaseComponent from ../components/BaseComponent;import PreviewPage from ./Layout/PreviewPage;import EditPage from ./Layout/EditPage;import {editMode, MODAL_TYPE, RIGHT_LAYOUT} from ../common/util;import ../common/lib;import ./App.scss;class App extends BaseComponent {}

  • 第一次編譯構建:

  • 增加一個新的依賴模塊 ../common/util2後重新構建編譯:

import {a} from ../common/util2;import React from react;import {connect} from react-redux;import EditModule from ./EditModule;import BaseComponent from ../components/BaseComponent;import PreviewPage from ./Layout/PreviewPage;import EditPage from ./Layout/EditPage;import {editMode, MODAL_TYPE, RIGHT_LAYOUT} from ../common/util;import ../common/lib;import ./App.scss;

我們並沒有修改依賴的第三方包,但是vender chunk的chunkHash也發生了更改。

導致這個結果的原因在於,由於引入了一個新模塊,使得打包過程中部分模塊的模塊ID發生了改變。而模塊ID的改變,直接導致了包含這些模塊的chunk內容改變,進而導致chunkHash的改變。

  • 引入util2.js

  • 引入 util2.js

穩定的模塊chunkhash

前面我們既然找到了問題的原因,那麼解決方案也就很明了了。那就是找到一種和順序無關的模塊ID命名方式。最容易想到的就是基於文件名或者文件內容的哈希值這兩種方案了。其實也就是今天要說的webpack內置的兩個pluginNamedModulesPlugin與HashedModuleIdsPlugin的功能。

啟用NamedModulesPlugin

這個模塊可以將依賴模塊的正整數 ID 替換為相對路徑(如:將 4 替換為 ./node_modules/es6-promise/dist/es6-promise.js)

  • 模塊相對路徑

  • 優點
  1. 開發模式,可以讓 webpack-dev-serverHMR 進行熱更新時在控制台輸出模塊名字而不是純數字
  2. 生產構建環境,可以避免因修改內容導致的 ID 變化,從而實現持久化緩存
  • 缺點
  1. 遞增 ID 替換為模塊相對路徑,構建出來的 chunk 會充滿各種路徑,使文件增大
  2. 模塊(npm 和自己的模塊)路徑會泄露,可能導致安全問題
  • 參考示例

啟用 HashedModuleIdsPlugin

NamedModulesPlugin 的進階模塊,它在其基礎上對模塊路徑進行 MD5 摘要,不僅可以實現持久化緩存,同時還避免了它引起的兩個問題(文件增大,路徑泄露)。因此可以輕鬆地實現 chunkhash 的穩定化!

  • 模塊路徑md5化

  • 官方文檔
  • 配置方法:

plugins: [ // 將基礎庫代碼提取到單獨文件中 new webpack.optimize.CommonsChunkPlugin({ name: vendor, minChunks: function (module, count) { // any required modules inside node_modules are extracted to vendor return ( module.resource && /.js$/.test(module.resource) && module.resource.indexOf( path.join(__dirname, ../node_modules) ) === 0 ) } }), new webpack.optimize.CommonsChunkPlugin({ name: manifest, chunks: [vendor] }), new webpack.HashedModuleIdsPlugin(), ]

構建編輯結果為:

  • 增加依賴模塊 ../common/util2後重新構建編譯:

可以看到啟用 HashedModuleIdsPlugin後,改變 App.jsx的依賴模塊後公共基礎模塊vendor.js的 chunkhash保持一致,從而也就能夠避免持久化緩存的失效。

  • 參考示例

參考資料

  • 用 webpack 實現持久化緩存
  • Webpack2中的NamedModulesPlugin與HashedModuleIdsPlugin
  • webpack緩存
  • webpack-module-ids

推薦閱讀:

[譯]Web 的現狀:網頁性能提升指南
小記JS模塊化
web前端和web後端的區別是什麼?新手必看
前端性能優化(一)用一張圖說明載入優化
【譯】理解關鍵渲染路徑

TAG:webpack | 前端性能優化 |