- 原文地址: use long term caching
- 原文做者: Ivan Akulov
- 譯文地址: 利用好持久化緩存
- 譯者: 周文康
- 校對者: 閆蒙、泥坤
在優化應用體積以後,下一個提高應用加載時間的策略就是緩存。將資源緩存在客戶端中,能夠避免以後每次都從新下載。html
使用緩存的通用方法:前端
告訴瀏覽器須要緩存一個文件很長時間(好比,一年)vue
# Server header
Cache-Control: max-age=31536000
複製代碼
⭐️ 注意:若是你不熟悉
Cache-Control
的原理,請參閱 Jake Archibald 的文章: 關於緩存的最佳實踐。node
當文件改變時,文件會被重命名,這樣就迫使瀏覽器從新下載:react
<!-- 修改前 -->
<script src="./index-v15.js"></script>
<!-- 修改後 -->
<script src="./index-v16.js"></script>
複製代碼
這個方法能夠告訴瀏覽器去下載 JS 文件,並將它緩存,以後使用的都是它的緩存副本。瀏覽器只會在文件名發生改變(或者一年以後緩存失效)時纔會請求網絡。webpack
使用 webpack,一樣能夠作到,但使用的不是版本號,而是指定文件的哈希值。使用 [chunkhash]
能夠將哈希值寫入文件名中:git
// webpack.config.js
module.exports = {
entry: './index.js',
output: {
filename: 'bundle.<strong>[chunkhash]</strong>.js',
// → bundle.8e0d62a03.js
},
};
複製代碼
⭐️ 注意: 即便 bundle 不變,webpack 也可能生成不一樣的哈希值 – 例如,你重命名了一個文件或者在不一樣的操做系統下編譯了 bundle。 固然,這實際上是一個 bug,目前尚未明確的解決方案,具體可參閱 GitHub 上的討論。github
若是你須要將文件名發送給客戶端,可使用 HtmlWebpackPlugin
或者 WebpackManifestPlugin
。web
HtmlWebpackPlugin
是一個簡單但擴展性不強的插件。在編譯期間,它會生成一個 HTML 文件,文件包含了全部已經被編譯的資源。若是你的服務端邏輯不是很複雜,那麼它應該能知足你:vue-router
<!-- index.html -->
<!doctype html> <!-- ... --> <script src="bundle.8e0d62a03.js"></script> 複製代碼
WebpackManifestPlugin
是一個擴展性更佳的插件,它能夠幫助你解決服務端邏輯比較複雜的那部分。在打包時,它會生成一個 JSON 文件,裏面包含了原文件名和帶哈希文件名的映射。在服務端,經過這個 JSON 就能方便的找到咱們真正要執行的文件:
// manifest.json
{
"bundle.js": "bundle.8e0d62a03.js"
}
複製代碼
應用的依賴一般比實際應用內的代碼變動頻率低。若是將它們移到單獨的文件中,瀏覽器就能夠獨立緩存它們 – 這樣每次應用中的代碼變動也不會去從新下載它們。
關鍵術語:在 webpack 術語中,把帶有應用代碼的獨立文件稱之爲 chunk。咱們在下面的文章中會使用到這個名稱。
要將依賴項提取到獨立的 chunk 中,須要執行下面三個步驟:
將輸出文件名替換爲[name].[chunkname].js
:
// webpack.config.js
module.exports = {
output: {
// Before
filename: 'bundle.[chunkhash].js',
// After
filename: '[name].[chunkhash].js',
},
};
複製代碼
當 webpack 編譯應用時,它會將[name]
做爲 chunk 的名稱。若是咱們沒有添加 [name]
的部分,咱們將不得不經過哈希值來區分 chunk - 這樣就變得很是困難!
將 entry
的值改成對象:
// webpack.config.js
module.exports = {
// Before
entry: './index.js',
// After
entry: {
main: './index.js',
},
};
複製代碼
在上面這段代碼中,「main」 是 chunk 的名稱。這個名稱會在第一步時被 [name]
所替代。
到目前爲止,若是你構建應用,這個 chunk 仍是包含了整個應用的代碼 - 就像咱們沒有作過上述這些步驟同樣。但接下來很快就將產生變化。
在 webpack 4 中,能夠將 optimization.splitChunks.chunks: 'all'
選項添加到 webpack 的配置中:
// webpack.config.js (for webpack 4)
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
}
},
};
複製代碼
這個選項能夠開啓智能代碼拆分。使用了這個功能,webpack 將會提取大於 30KB(壓縮和 gzip 以前)的第三方庫代碼。它同時也能夠提取公共代碼 - 若是你的構建結果會生成多個 bundle 時這將很是有用。(例如:假如你經過路由來拆分應用)。
在 webpack 3 中添加 CommonsChunkPlugin 插件:
// webpack.config.js (for webpack 3)
module.exports = {
plugins: [
new webpack.optimize.CommonsChunkPlugin({
// chunk 的名稱將會包含依賴
// 這個名稱會在第一步時被 [name] 所替代
name: 'vendor',
// 這個函數決定哪一個模塊會被打入 chunk
minChunks: module => module.context &&
module.context.includes('node_modules'),
}),
],
};
複製代碼
這個插件會將路徑包含 node_modules
的全部模塊移到一個名爲 vendor.[chunkhash].js 的獨立文件中。
完成這些更改後,每次打包都將從原來的生成一個文件變爲生成兩個文件:main.[chunkhash].js
和vendor.[chunkhash].js
(vendors~main.[chunkhash].js
只有在 webpack 4 纔有)。在 webpack 4 中,若是依賴項很小,則可能不會生成 vendor bundle - 這點作的不錯:
$ webpack
Hash: ac01483e8fec1fa70676
Version: webpack 3.8.1
Time: 3816ms
Asset Size Chunks Chunk Names
./main.00bab6fd3100008a42b0.js 82 kB 0 [emitted] main
./vendor.d9e134771799ecdf9483.js 47 kB 1 [emitted] vendor
複製代碼
瀏覽器會單獨緩存這些文件 - 同時只有代碼發生改變時纔會從新下載。
遺憾的是,僅僅提取第三方庫代碼仍是不夠的。若是你想嘗試在應用代碼中修改一些東西:
// index.js
…
…
// 例如,增長這句:
console.log('Wat');
複製代碼
你會發現 vendor
的哈希值也會被改變:
Asset Size Chunks Chunk Names
./vendor.d9e134771799ecdf9483.js 47 kB 1 [emitted] vendor
複製代碼
↓
Asset Size Chunks Chunk Names
./vendor.e6ea4504d61a1cc1c60b.js 47 kB 1 [emitted] vendor
複製代碼
這是因爲 webpack 打包時,除了模塊代碼以外,webpack 的 bundle 中還包含了 runtime - 一小段能夠管理模塊執行的代碼。當你將代碼拆分紅多個文件時,這小部分代碼在 chunk id 和匹配的文件之間會生成一個映射:
// vendor.e6ea4504d61a1cc1c60b.js
script.src = __webpack_require__.p + chunkId + "." + {
"0": "2f2269c7f0a55a5c1871"
}[chunkId] + ".js";
複製代碼
Webpack 將 runtime 包含在了最新生成的 chunk 中,這個 chunk 就是咱們代碼中的 vendor
。每次 chunk 有任何變動,這一小部分代碼也會隨之更改,同時也會致使整個 vendor
chunk 發生改變。
爲了解決這個問題,咱們能夠將 runtime 移動到一個獨立的文件中。在 webpack 4 中,能夠經過開啓 optimization.runtimeChunk
選項來實現:
// webpack.config.js (for webpack 4)
module.exports = {
optimization: {
runtimeChunk: true,
},
};
複製代碼
在 webpack 3 中,能夠經過 CommonsChunkPlugin
建立一個額外的空 chunk:
// webpack.config.js (for webpack 3)
module.exports = {
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks: module => module.context &&
module.context.includes('node_modules'),
}),
// 這個插件必須在 vendor 生成以後執行(由於 webpack 把運行時打進了最新的 chunk)
new webpack.optimize.CommonsChunkPlugin({
name: 'runtime',
// minChunks: Infinity 表示任何應用模塊都不能打進這個 chunk
minChunks: Infinity,
}),
],
};
複製代碼
完成這些變動後,每次構建將生成三個文件:
$ webpack
Hash: ac01483e8fec1fa70676
Version: webpack 3.8.1
Time: 3816ms
Asset Size Chunks Chunk Names
./main.00bab6fd3100008a42b0.js 82 kB 0 [emitted] main
./vendor.26886caf15818fa82dfa.js 46 kB 1 [emitted] vendor
./runtime.79f17c27b335abc7aaf4.js 1.45 kB 3 [emitted] runtime
複製代碼
將這幾個文件按倒序的方式添加到 index.html
中,就完成了:
<!-- index.html -->
<script src="./runtime.79f17c27b335abc7aaf4.js"></script>
<script src="./vendor.26886caf15818fa82dfa.js"></script>
<script src="./main.00bab6fd3100008a42b0.js"></script>
複製代碼
optimization.splitChunks
和 optimization.runtimeChunk
的工做原理爲了達到更好的體驗,咱們能夠嘗試把 webpack 的 runtime 內聯到 HTML 中。例如,咱們不要這麼作:
<!-- index.html -->
<script src="./runtime.79f17c27b335abc7aaf4.js"></script>
複製代碼
而是像下面這樣:
<!-- index.html -->
<script> !function(e){function n(r){if(t[r])return t[r].exports;…}} ([]); </script>
複製代碼
Runtime 的代碼很少,內聯到 HTML 中能夠幫助咱們節省 HTTP 請求(在 HTTP/1 中尤其重要;在 HTTP/2 中雖然沒那麼重要,但仍然能起到必定做用)。
下面就來看看要如何作。
若是你使用 HtmlWebpackPlugin 來生成 HTML 文件,那麼你必定須要 InlineSourcePlugin :
// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const InlineSourcePlugin = require('html-webpack-inline-source-plugin');
module.exports = {
plugins: [
new HtmlWebpackPlugin({
// Inline all files which names start with 「runtime~」 and end with 「.js」.
// That’s the default naming of runtime chunks
inlineSource: 'runtime~.+\\.js',
}),
// This plugin enables the 「inlineSource」 option
new InlineSourcePlugin(),
],
};
複製代碼
在 webpack 4 中:
添加 WebpackManifestPlugin 插件能夠獲取生成的 runtume chunk 的名稱:
// webpack.config.js (for webpack 4)
const ManifestPlugin = require('webpack-manifest-plugin');
module.exports = {
plugins: [
new ManifestPlugin(),
],
};
複製代碼
使用這個插件構建會生成像下面這樣的文件:
// manifest.json
{
"runtime~main.js": "runtime~main.8e0d62a03.js"
}
複製代碼
能夠用一個便利的方式內聯 runtime chunk 的內容。例如,使用 Node.js 和 Express:
// server.js
const fs = require('fs');
const manifest = require('./manifest.json');
const runtimeContent = fs.readFileSync(manifest['runtime~main.js'], 'utf-8');
app.get('/', (req, res) => {
res.send(` … <script>${runtimeContent}</script> … `);
});
複製代碼
在 webpack 3 中:
經過指定 filename
,可使 runtime 的名稱不發生改變 :
// webpack.config.js (for webpack 3)
module.exports = {
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'runtime',
minChunks: Infinity,
filename: 'runtime.js',
// → Now the runtime file will be called
// 「runtime.js」, not 「runtime.79f17c27b335abc7aaf4.js」
}),
],
};
複製代碼
能夠用一個便利的方式內聯 runtime.js
的內容。例如,使用 Node.js 和 Express:
// server.js
const fs = require('fs');
const runtimeContent = fs.readFileSync('./runtime.js', 'utf-8');
app.get('/', (req, res) => {
res.send(` … <script>${runtimeContent}</script> … `);
});
複製代碼
一般,一個網頁會有自身的側重點:
上面的這些狀況,均可以經過優先下載最重要的部分,稍後懶加載剩餘部分,從而來提高頁面首次加載的性能。在 webpack 中,使用import()
函數 和代碼拆分便可實現。
// videoPlayer.js
export function renderVideoPlayer() { … }
// comments.js
export function renderComments() { … }
// index.js
import {renderVideoPlayer} from './videoPlayer';
renderVideoPlayer();
// …Custom event listener
onShowCommentsClick(() => {
import('./comments').then((comments) => {
comments.renderComments();
});
});
複製代碼
import()
函數能夠幫助你實現按需加載。Webpack 在打包時遇到 import('./module.js')
,就會把這個模塊放到單獨的 chunk 中:
$ webpack
Hash: 39b2a53cb4e73f0dc5b2
Version: webpack 3.8.1
Time: 4273ms
Asset Size Chunks Chunk Names
./0.8ecaf182f5c85b7a8199.js 22.5 kB 0 [emitted]
./main.f7e53d8e13e9a2745d6d.js 60 kB 1 [emitted] main
./vendor.4f14b6326a80f4752a98.js 46 kB 2 [emitted] vendor
./runtime.79f17c27b335abc7aaf4.js 1.45 kB 3 [emitted] runtime
複製代碼
只有當代碼執行到 import()
函數時纔會去下載。
這樣可讓 入口
bundle 變得更小,從而減小首次加載時間。不只如此,它還能夠優化緩存 - 若是你修改了入口 chunk 的代碼,註釋 chunk 不會受到影響。
⭐️ 注意: 若是你使用 Babel 編譯代碼,會由於 Babel 沒法識別
import()
而出現語法錯誤。爲了不這個錯誤,你能夠添加syntax-dynamic-import
插件。
import()
函數的使用import()
語法若是你的應用有多個路由或頁面,可是代碼中只有一個單獨的 JS 文件(一個單獨的入口
chunk),這樣彷佛會讓你的每次請求都附加了額外的流量。例如,當用戶訪問你網站的首頁:
他們並不須要加載其它頁面上用於渲染文章的代碼 - 但他們卻加載了。此外,若是這個用戶常常只是訪問首頁,但你更改了其它頁面的文章代碼,webpack 將會從新編譯,使整個 bundle 失效 - 這樣將致使用戶從新下載整個應用的代碼。
若是咱們將代碼拆分到頁面中(或者單頁面應用的路由裏),用戶就會只下載真正用到的那部分代碼。此外,瀏覽器也會更好地緩存應用代碼:當你改變首頁的代碼時,webpack 只會讓相匹配的 chunk 失效。
要經過路由來拆分單頁應用,可使用 import()
(參加上文代碼懶加載部分)。若是你使用的是一個框架,目前也有現成的解決方案:
要經過頁面來拆分傳統應用,可使用 webpack 的 entry points。假設你的應用中有三類頁面:主頁、文章頁和用戶帳戶頁,- 那麼就應該有三個入口:
// webpack.config.js
module.exports = {
entry: {
home: './src/Home/index.js',
article: './src/Article/index.js',
profile: './src/Profile/index.js'
},
};
複製代碼
對於每一個入口文件,webpack 將構建一個單獨的依賴樹並生成一個 bundle,這個 bundle 裏只有包含這個入口所使用到的模塊:
$ webpack
Hash: 318d7b8490a7382bf23b
Version: webpack 3.8.1
Time: 4273ms
Asset Size Chunks Chunk Names
./0.8ecaf182f5c85b7a8199.js 22.5 kB 0 [emitted]
./home.91b9ed27366fe7e33d6a.js 18 kB 1 [emitted] home
./article.87a128755b16ac3294fd.js 32 kB 2 [emitted] article
./profile.de945dc02685f6166781.js 24 kB 3 [emitted] profile
./vendor.4f14b6326a80f4752a98.js 46 kB 4 [emitted] vendor
./runtime.318d7b8490a7382bf23b.js 1.45 kB 5 [emitted] runtime
複製代碼
因此,若是隻有 article 頁面使用到了 Lodash,那麼 home 和 profile bundle 就不會包含它 - 用戶也不會在訪問首頁的時候下載到這個庫。
可是,單獨的依賴樹有它們的缺點。若是兩個入口都使用到了 Lodash,同時你沒有將依賴項移到 vendor bundle 中,則兩個入口都將包含 Lodash 的副本。爲了解決這個問題,在 webpack 4 中,能夠在你的 webpack 配置中加入optimization.splitChunks.chunks: 'all'
選項:
// webpack.config.js (適用於webpack 4)
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
}
},
};
複製代碼
這個選項能夠開啓智能代碼拆分。有了這個選項,webpack 將自動查找到公共代碼,而且提取到單獨的文件中。
在 webpack 3 中,可使用 CommonsChunkPlugin
插件,它會將公共的依賴項移動到一個新的指定文件中:
// webpack.config.js (適用於 webpack 3)
module.exports = {
plugins: [
new webpack.optimize.CommonsChunkPlugin({
// chunk 的名稱將會包含公共依賴
name: 'common',
// minChunks表示要將一個模塊打入公共文件時必須包含的 `minChunks` chunks 數量
// (注意,插件會分析全部 chunks 和 entries)
minChunks: 2, // 2 is the default value
}),
],
};
複製代碼
你能夠嘗試調整 minChunks
的值來找到最優的方案。一般狀況下,你但願它是一個較小的值,但隨着 chunk 數量的增長它會隨之增大。例如,有 3 個 chunk 時,minChunks
的值多是 2 ,可是有 30 個 chunk 時,它的值多是 8 - 由於若是你把它設置成 2,就會有不少模塊要被打包進同一個公共文件中,這樣文件就會變得臃腫。
optimization.splitChunks
和 optimization.runtimeChunk
的工做原理構建代碼時,webpack 會爲每一個模塊分配一個 ID。隨後,這些 ID 將在 bundle 裏的 require()
函數中被使用到。你一般會在編譯輸出的模塊路徑前看到這些 ID:
$ webpack
Hash: df3474e4f76528e3bbc9
Version: webpack 3.8.1
Time: 2150ms
Asset Size Chunks Chunk Names
./0.8ecaf182f5c85b7a8199.js 22.5 kB 0 [emitted]
./main.4e50a16675574df6a9e9.js 60 kB 1 [emitted] main
./vendor.26886caf15818fa82dfa.js 46 kB 2 [emitted] vendor
./runtime.79f17c27b335abc7aaf4.js 1.45 kB 3 [emitted] runtime
複製代碼
↓ 看下面
[0] ./index.js 29 kB {1} [built]
[2] (webpack)/buildin/global.js 488 bytes {2} [built]
[3] (webpack)/buildin/module.js 495 bytes {2} [built]
[4] ./comments.js 58 kB {0} [built]
[5] ./ads.js 74 kB {1} [built]
+ 1 hidden module
複製代碼
默認狀況下,這些 ID 是使用計數器計算出來的(例如,第一個模塊的 ID 是 0,第二個模塊的 ID 就是 1,以此類推)。但這樣作有個問題,當你新增一個模塊時,它會可能出如今模塊列表的中間,從而致使以後全部模塊的 ID 都被改變:
$ webpack
Hash: df3474e4f76528e3bbc9
Version: webpack 3.8.1
Time: 2150ms
Asset Size Chunks Chunk Names
./0.5c82c0f337fcb22672b5.js 22 kB 0 [emitted]
./main.0c8b617dfc40c2827ae3.js 82 kB 1 [emitted] main
./vendor.26886caf15818fa82dfa.js 46 kB 2 [emitted] vendor
./runtime.79f17c27b335abc7aaf4.js 1.45 kB 3 [emitted] runtime
[0] ./index.js 29 kB {1} [built]
[2] (webpack)/buildin/global.js 488 bytes {2} [built]
[3] (webpack)/buildin/module.js 495 bytes {2} [built]
複製代碼
↓ 咱們添加了一個新模塊...
[4] ./webPlayer.js 24 kB {1} [built]
複製代碼
↓ 看看下面作了什麼! comments.js
的 ID 由 4 變成了 5
[5] ./comments.js 58 kB {0} [built]
複製代碼
↓ ads.js
的 ID 由 5 變成了 6
[6] ./ads.js 74 kB {1} [built]
+ 1 hidden module
複製代碼
這將使包含或依賴於這些被更改 ID 的模塊的全部 chunk 都無效 - 即便它們實際代碼沒有更改。在咱們的案例中,ID 爲 0
的 chunk ( comments.js
的 chunk) 和 main
chunk (其它應用代碼的 chunk )都將失效 - 但其實只有 main
應該失效。
爲了解決這個問題,可使用 HashedModuleIdsPlugin
插件來改變模塊 ID 的計算方式。這個插件用模塊路徑的哈希值代替了基於計數器的 ID:
$ webpack
Hash: df3474e4f76528e3bbc9
Version: webpack 3.8.1
Time: 2150ms
Asset Size Chunks Chunk Names
./0.6168aaac8461862eab7a.js 22.5 kB 0 [emitted]
./main.a2e49a279552980e3b91.js 60 kB 1 [emitted] main
./vendor.ff9f7ea865884e6a84c8.js 46 kB 2 [emitted] vendor
./runtime.25f5d0204e4f77fa57a1.js 1.45 kB 3 [emitted] runtime
複製代碼
↓ 看下面
[3IRH] ./index.js 29 kB {1} [built]
[DuR2] (webpack)/buildin/global.js 488 bytes {2} [built]
[JkW7] (webpack)/buildin/module.js 495 bytes {2} [built]
[LbCc] ./webPlayer.js 24 kB {1} [built]
[lebJ] ./comments.js 58 kB {0} [built]
[02Tr] ./ads.js 74 kB {1} [built]
+ 1 hidden module
複製代碼
使用了這個方法,只有當重命名或移動該模塊時,模塊的 ID 纔會更改。新的模塊也不會影響到其餘模塊的 ID。
能夠在配置中的 plugins
部分開啓這個插件:
// webpack.config.js
module.exports = {
plugins: [
new webpack.HashedModuleIdsPlugin(),
],
};
複製代碼
import
懶加載非關鍵代碼