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)
- 模塊相對路徑
- 優點
- 開發模式,可以讓 webpack-dev-server 和 HMR 進行熱更新時在控制台輸出模塊名字而不是純數字
- 生產構建環境,可以避免因修改內容導致的 ID 變化,從而實現持久化緩存
- 缺點
- 遞增 ID 替換為模塊相對路徑,構建出來的 chunk 會充滿各種路徑,使文件增大
- 模塊(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後端的區別是什麼?新手必看
※前端性能優化(一)用一張圖說明載入優化
※【譯】理解關鍵渲染路徑