系列第二篇,來看看基于 React 路由分塊的頁(yè)面加載優(yōu)化。
- 原文作者:Addy Osmani
- 譯文出自:掘金翻譯計(jì)劃
- 譯者:markzhai
使用 React.js 的漸進(jìn)式 Web 應(yīng)用程序:第 2 部分 - 頁(yè)面加載性能
新 系列 的第二部分會(huì)完整地走一遍怎么使用 Lighthouse. 來優(yōu)化移動(dòng) web apps。這篇文章,我們來看看頁(yè)面加載性能。
保證頁(yè)面加載性能是快的
移動(dòng) Web 的速度很關(guān)鍵。平均地,更快的體驗(yàn)會(huì)帶來 70% 更長(zhǎng)的會(huì)話 以及兩倍以上更多的移動(dòng)廣告收益。Web 性能的投資像是基于 React 的 Flipkart Lite 獲得了三倍網(wǎng)站瀏覽時(shí)間, GQ 在流量上得到了 80% 增長(zhǎng),Trainline 在 年收益上增長(zhǎng)了 11M 并且 Instagram 增長(zhǎng)了 33% 的印象.
在你的 web app 加載時(shí)有一些 關(guān)鍵的用戶時(shí)刻:
測(cè)量然后優(yōu)化總是關(guān)鍵的。Lighthouse 的頁(yè)面加載檢測(cè)會(huì)關(guān)注:
- 第一次有意義的繪制(當(dāng)頁(yè)面主內(nèi)容可見)
- 速度指數(shù)(Speed Index)(可見區(qū)域完整)
- 估算的輸入延遲(當(dāng)主線程可以立即處理用戶輸入)
- 以及 抵達(dá)可交互的時(shí)間(app 多快達(dá)到可用可參與)
順帶一提,Paul Irish 做了很了不起的相關(guān)總結(jié) PWAs 的有趣指標(biāo)值得一看。
良好性能的目標(biāo):
- 遵循 RAIL 性能模型 的 L 部分。** A+ 的性能是我們所有人都必須力求達(dá)到的,即便有的瀏覽器不支持 Service Worker。我們?nèi)匀豢梢钥焖俚卦谄聊簧汐@得一些有意義的內(nèi)容,并且僅加載我們所需要的**
- 在典型網(wǎng)絡(luò)(3G)和硬件條件下
- 首次訪問在 5 秒內(nèi)可交互,重復(fù)訪問(Service Worker 可用)則在 2 秒內(nèi)。
- 首次加載(網(wǎng)絡(luò)限制下),速度指數(shù)在 3000 或者更少。
- 第二次加載(磁盤限制,因?yàn)?Service Worker 可用):速度指數(shù) 1000 或者更少。
讓我們?cè)僬f說,關(guān)于通過 TTI 關(guān)注交互性。
關(guān)注抵達(dá)可交互時(shí)間(TTI)
為交互性優(yōu)化,也就是使得 app 盡快能對(duì)用戶可用(比如讓他們可以四處點(diǎn)擊,app 可以相應(yīng))。這對(duì)試圖在移動(dòng)設(shè)備上提供一流用戶體驗(yàn)的現(xiàn)代 web 體驗(yàn)很關(guān)鍵。
Lighthouse 現(xiàn)在將 TTI 測(cè)量為布局穩(wěn)定,web 字體可見,并且主線程可以響應(yīng)用戶輸入的時(shí)間。有很多方法來手動(dòng)跟蹤 TTI,重要的是根據(jù)指標(biāo)進(jìn)行優(yōu)化會(huì)提升你用戶的體驗(yàn)。
對(duì)于像 React 這樣的庫(kù),你應(yīng)該關(guān)心的是在移動(dòng)設(shè)備上 啟用庫(kù)的代價(jià) 因?yàn)檫@會(huì)讓人們有感知。在 ReactHN,我們達(dá)到了 1700毫秒 內(nèi)可交互,通過保持整個(gè) app 的大小和執(zhí)行代價(jià)相對(duì)小,撇開有多個(gè)視圖:app bundle gzipped 壓縮后 11KB,107KB 用于我們的 vendor/React/庫(kù) bundle,實(shí)踐中有點(diǎn)像這樣:
之后,對(duì)于功能顆粒狀的 apps,我們會(huì)看看性能模式像是 PRPL,通過在 HTTP/2 服務(wù)器 Push 下利用顆粒狀的 “基于路由的分塊” 來得到快速的可交互時(shí)間。(可以試試 Shop demo 來看看我們說的是什么)。
Housing.com 最近使用了類 PRPL 模式搭載 React 體驗(yàn),獲得了很多贊揚(yáng):
Housing.com 利用 Webpack 路由分塊,來推遲入口頁(yè)面的部分啟動(dòng)消耗(僅加載 route 渲染所需要的)。更多細(xì)節(jié)請(qǐng)查看 Sam Saccone 的優(yōu)秀 Housing.com 性能檢測(cè).
Flipkart 也做了類似的:
注意:關(guān)于什么是 “到可交互時(shí)間”,有很多不同的看法,Lighthouse 對(duì) TTI 的定義也可能會(huì)演變。其他跟蹤的方法有導(dǎo)航后第一個(gè) 5 秒內(nèi) window 沒有長(zhǎng)任務(wù)的時(shí)刻,或者一次文本/內(nèi)容繪制后第一次 5 秒內(nèi) window 沒有長(zhǎng)任務(wù)的時(shí)刻?;旧?,就是頁(yè)面穩(wěn)定后多快,用戶可以和 app 交互的。
注意:盡管不是強(qiáng)制的要求,你可能也需要提高視覺完整度(速度指數(shù)),通過 優(yōu)化關(guān)鍵渲染路徑。關(guān)鍵路徑 CSS 優(yōu)化工具的存在 以及其優(yōu)化在 HTTP/2 的世界中依然有效。
基于路由分塊以提高性能
Webpack
如果你第一次接觸模塊打包工具像是 Webpack,看看 JS 模塊化打包器(視頻) 可能會(huì)有幫助。
一些今天的 JavaScript 工具使得將你的所有腳本打包成一個(gè) bundle.js 文件并包含所有頁(yè)面變得簡(jiǎn)單。這意味著很多時(shí)候,你可能要加載很多對(duì)當(dāng)前路由來說并不需要的代碼。為什么一次路由需要加載 500KB 的 JS,而事實(shí)上 50KB 就夠了呢?我們應(yīng)該丟開那些無助于獲得更快體驗(yàn)的腳本,來加速獲得可交互的路由。
當(dāng)僅提供用戶一次 route 所需要的最小功能可達(dá)代碼就可以的時(shí)候,避免提供龐大整塊的 bundles(像上圖)。
代碼分割是解決整塊的 bundles 的一個(gè)方法。想法大致是在你的代碼中定義分割點(diǎn),然后分割成不同的文件進(jìn)行按需懶加載。這會(huì)提升啟動(dòng)時(shí)間,幫助我們更快地可交互。
想象使用一個(gè)公寓列表 app。如果我們登陸的路由是列出我們所在區(qū)域的地產(chǎn)(route-1)—— 我們不需要查看完整的地產(chǎn)詳情的代碼(route-2)或者預(yù)約一次看房(route-3),所以我們可以僅提供用戶列表路由所需要的 JavaScript,然后動(dòng)態(tài)加載剩下的。
這些年來,代碼分割的想法已經(jīng)被很多 apps 使用,但現(xiàn)在用 “基于路由的分塊” 來稱呼它。通過 Webpack 模塊打包器,我們可以啟用 React 上的安裝。
實(shí)踐基于路由的代碼分塊
Webpack 支持當(dāng)它發(fā)現(xiàn)一個(gè) require.ensure() 被使用的時(shí)候?qū)⒛愕?app 代碼分割成塊(或者在 Webpack 2,一個(gè) System.import)。這些被稱為 “分割點(diǎn)”,Webpack 會(huì)對(duì)它們的每一個(gè)都生成一個(gè)分開的 bundlea,按需解決依賴。
// 定義一個(gè) "split-point"
require.ensure([], function () {
const details = require('./Details');
// 所有被 require() 需要的都會(huì)成為分開的 bundle
// require(deps, cb) 是異步的。它會(huì)異步加載,并且評(píng)估
// 模塊,通過你的 deps 的 exports 調(diào)用 cb。
});
當(dāng)你的代碼需要某些東西,Webpack 會(huì)發(fā)起一個(gè) JSONP 請(qǐng)求來從服務(wù)器獲得它。這個(gè)和 React Router 結(jié)合工作得很好,我們可以在對(duì)用戶渲染視圖之前在依賴(塊)中懶加載一個(gè)新的路由。
Webpack 2 支持 使用 React Router 的自動(dòng)代碼分割 因?yàn)樗梢蕴幚砟K上的 System.import 調(diào)用為 import 語句,將導(dǎo)入的文件和它們的依賴一起打包。依賴不會(huì)與你在 Webpack 設(shè)置中的初始入口沖突。
import App from '../containers/App';
function errorLoading(err) {
console.error('Lazy-loading failed', err);
}
function loadRoute(cb) {
return (module) => cb(null, module.default);
}
export default {
component: App,
childRoutes: [
// ...
{
path: 'booktour',
getComponent(location, cb) {
System.import('../pages/BookTour')
.then(loadRoute(cb))
.catch(errorLoading);
}
}
]
};
附錄:預(yù)加載那些路由!
在我們繼續(xù)之前,除了剛才的方法,另一個(gè)可選的是 來自 Resource Hints。這個(gè)提供給我們一個(gè)方法來宣告式地獲取資源而不執(zhí)行它們。預(yù)加載可以用來預(yù)加載用戶可能要去的路由所需要的 Webpack 塊,于是緩存已經(jīng)為他們準(zhǔn)備好了,可以在需要的時(shí)候立即可用。
在寫的時(shí)候,預(yù)加載只能在 Chrome 中進(jìn)行,但是在支持的瀏覽器中被處理為漸進(jìn)式增加。
注意:html-webpack-plugin 的 模板和自定義事件 可以使用最小的改變來讓簡(jiǎn)化這個(gè)過程。然后你應(yīng)該保證預(yù)加載的資源真正會(huì)對(duì)你大部分的用戶瀏覽過程有用。
異步加載路由
讓我們回到代碼分割(code-splitting)—— 在一個(gè)使用 React 和 React Router 的 app 里,我們可以使用 require.ensure() 以在 ensure 被調(diào)用的時(shí)候異步加載一個(gè)組件。順帶一提,如果任何人在探索服務(wù)器渲染,這個(gè)在 Node 里需要被 node-ensure 包來填充。Pete Hunt 在 Webpack How-to 涵蓋了異步加載。
在下面的例子里,require.ensure() 使我們可以按需懶加載路由,在組件被使用前等待拉取:
const rootRoute = {
component: Layout,
path: '/',
indexRoute: {
getComponent (location, cb) {
require.ensure([], () => {
cb(null, require('./Landing'))
})
}
},
childRoutes: [
{
path: 'book',
getComponent (location, cb) {
require.ensure([], () => {
cb(null, require('./BookTour'))
})
}
},
{
path: 'details/:id',
getComponent (location, cb) {
require.ensure([], () => {
cb(null, require('./Details'))
})
}
}
]
}
注意:我經(jīng)常通過 CommonChunksPlugin(minChunks:
Infinity)來進(jìn)行上面的安裝,所以在我不同的入口點(diǎn)之間有一個(gè)帶有通用模塊的 chunk。這也 極力降低了 陷入缺省 Webpack runtime 的可能。
Brian Holt 在 React 的完整介紹 對(duì)異步路由加載涵蓋得很好。通過異步路由的代碼分割在 React Router 的目前版本和 新的 React Router V4 都可以使用。
通過異步的 getComponent + require.ensure() 的簡(jiǎn)單宣告式路由分塊
這有有一個(gè)小貼士,可以使代碼分割的安裝更快。在 React Router,一個(gè) 宣告式的路由 來將路由 “/” 映射到組件 App 看上去像 <Route path=”/” component={App}>.
React Router 也支持一個(gè)方便的 [getComponent](https://github.com/ReactTraining/react-router/blob/master/docs/API.md#getcomponentnextstate-callback) 屬性,類似于 component 但卻是異步的,對(duì)快速安裝上代碼分割超級(jí)有用:
<Route
path="stories/:storyId"
getComponent={(nextState, cb) => {
// 異步地查找 components
cb(null, Stories)
}} />
getComponent 函數(shù)參數(shù)包括下一個(gè)狀態(tài)(我設(shè)置為 null)和一個(gè)回調(diào)。
讓我們添加一些基于路由的代碼分割到 ReactHN。我們會(huì)從 routes 文件中的一段開始?——?它定義了組件的 require 調(diào)用和對(duì)每個(gè)路由的 React Router 路由(比如 news, item, poll, job, comment 永久鏈接等):
var IndexRoute = require('react-router/lib/IndexRoute')
var App = require('./App')
var Item = require('./Item')
var PermalinkedComment = require('./PermalinkedComment') <--
var UserProfile = require('./UserProfile')
var NotFound = require('./NotFound')
var Top = stories('news', 'topstories', 500)
// ....
module.exports = <Route path="/" component={App}>
<IndexRoute component={Top}/>
<Route path="news" component={Top}/>
<Route path="item/:id" component={Item}/>
<Route path="job/:id" component={Item}/>
<Route path="poll/:id" component={Item}/>
<Route path="comment/:id" component={PermalinkedComment}/> <---
<Route path="newcomments" component={Comments}/>
<Route path="user/:id" component={UserProfile}/>
<Route path="*" component={NotFound}/>
</Route>
ReactHN 現(xiàn)在提供給用戶一個(gè)整塊包含所有路由的 JS bundle。讓我們將它轉(zhuǎn)換為路由分塊,只提供一次路由真正需要的代碼,從 comment 的永久鏈接開始(comment/:id):
所以我們首先刪了對(duì)永久鏈接組件的隱式 require:
var PermalinkedComment = require(‘./PermalinkedComment’)
然后開始我們的路由..
然后使用宣告式的 getComponent 來更新它。我們?cè)诼酚芍惺褂?require.ensure() 調(diào)用來懶加載,而這就是我們所需要做的一切了:
<Route
path="comment/:id"
getComponent={(location, callback) => {
require.ensure([], require => {
callback(null, require('./PermalinkedComment'))
}, 'PermalinkedComment')
}}
/>
Orz 太美了。這..就搞定了。不騙你。我們可以把這個(gè)用到剩下的路由上,然后運(yùn)行 webpack。它會(huì)正確地找到 require.ensure() 調(diào)用,然后如我們想要地區(qū)分隔代碼。
在應(yīng)用宣告式的代碼分割到更多我們的路由后,可以看到路由分塊在運(yùn)作,僅僅會(huì)加載一次路由所需要的代碼(在 Service Worker 可以預(yù)緩存起來):
提醒:有許多可用于 Service Worker 的簡(jiǎn)單 Webpack 插件:
- sw-precache-webpack-plugin 在底層使用 sw-precache
- offline-plugin 被 react-boilerplate 所使用
CommonsChunkPlugin
為了識(shí)別出在不同路由使用的通用模塊并把它們放在一個(gè)通用的分塊,需要使用 CommonsChunkPlugin。它需要在每個(gè)頁(yè)面 requires 兩個(gè) script 標(biāo)簽,一個(gè)用于 commons 分塊,另一個(gè)用于一次路由的入口分塊。
const CommonsChunkPlugin = require("webpack/lib/optimize/CommonsChunkPlugin");
module.exports = {
entry: {
p1: "./route-1",
p2: "./route-2",
p3: "./route-3"
},
output: {
filename: "[name].entry.chunk.js"
},
plugins: [
new CommonsChunkPlugin("commons.chunk.js")
]
}
Webpack 的?—?display-chunks 標(biāo)志 對(duì)于查看模塊在哪個(gè)分塊中出現(xiàn)很有用。這個(gè)幫助我們減少分塊中重復(fù)的依賴,并且能提示在你的項(xiàng)目中開啟 CommonChunksPlugin 是否值得。這是一個(gè)帶有多個(gè)組件的項(xiàng)目,在不同分塊間檢測(cè)到重復(fù)的 Mustache.js 依賴:
Webpack 1 也支持通過 DedupePlugin 以在你的依賴樹中進(jìn)行依賴庫(kù)的去重。在 Webpack 2,tree-shaking 應(yīng)該淘汰了這個(gè)的需求。
更多 Webpack 的小貼士
- 你的代碼庫(kù)中 require.ensure() 調(diào)用的數(shù)目通常會(huì)關(guān)聯(lián)到生成的 bundles 的數(shù)目。在代碼庫(kù)中大量使用 ensure 的時(shí)候意識(shí)到這點(diǎn)很有用。
- Webpack2 的 Tree-shaking 會(huì)幫助刪除沒用的 exports,這可以讓你的 bundle 尺寸變小。
- 另外,小心避免在 通用/共享的 bundles 里面調(diào)用 require.ensure()。你可能會(huì)發(fā)現(xiàn)這會(huì)創(chuàng)建入口點(diǎn)引用,關(guān)于那些已經(jīng)被加載了的依賴的假設(shè)。
- 在 Webpack 2,System.import 目前不支持服務(wù)端渲染,但我已經(jīng)在 StackOverflow 分享了怎么去處理這個(gè)問題。
- 如果需要優(yōu)化編譯速度,可以看看 Dll plugin,parallel-webpack 以及目標(biāo)的編譯。
- 如果你希望通過 Webpack 異步 或者 延遲 腳本,看看 script-ext-html-webpack-plugin
在 Webpack 編譯中檢測(cè)臃腫
Webpack 社區(qū)有很多建立在 Web 上的編譯分析器包括 http://webpack.github.io/analyse/,https://chrisbateman.github.io/webpack-visualizer/,和 https://alexkuz.github.io/stellar-webpack/。這些對(duì)于理解你最大的模塊是什么很有用。
source-map-explorer (來自 Paul Irish) 通過 source maps 來理解代碼臃腫,也超級(jí)棒的??纯催@個(gè)對(duì) ReactHN Webpack bundle 的 tree-map 可視化,帶有每個(gè)文件的代碼行數(shù),以及百分比的統(tǒng)計(jì)分析:
你可能也會(huì)對(duì)來自 Sam Saccone 的 coverage-ext 感興趣,它可以用來對(duì)任何 webapp 生成代碼覆蓋率。這個(gè)對(duì)于理解你的代碼中有多少實(shí)際會(huì)被執(zhí)行到很有用。
代碼分割(code-splitting)之上:PRPL 模式
Polymer 發(fā)現(xiàn)了一個(gè)有趣的 web 性能模式,用于精細(xì)服務(wù)的 apps,稱為 PRPL(看看 Kevin 的 I/O 演講)。這個(gè)模式嘗試為交互性優(yōu)化,并且代表了:
- (P)ush,對(duì)于初始路由推送關(guān)鍵資源。
- (R)ender,渲染初始路由,并使它盡快變得可交互。
- (P)re-cache,通過 Service Worker 預(yù)緩存剩下的路由。
- (L)azy-load,根據(jù)用戶在應(yīng)用中的移動(dòng)懶加載并懶初始化 apps 中對(duì)應(yīng)的部分。
在這里,我們必須給予 Polymer Shop demo 大大的贊賞,因?yàn)樗蛭覀冋故玖苏鎸?shí)移動(dòng)設(shè)備上的道路。使用 PRPL(在這種情況下通過 HTML Imports,從而利用瀏覽器的后臺(tái) HTML parser 的好處)。屏幕上的像素你都可以使用。這里額外的工作在于分塊和保持可交互。在一臺(tái)真實(shí)移動(dòng)設(shè)備上,我們可以在 1.75 秒內(nèi)達(dá)到可交互。1.3 秒用于 JavaScript,但它都被打散了。在那以后所有功能都可以用了。
你到現(xiàn)在應(yīng)該已經(jīng)成功享受到講應(yīng)用打碎到更精細(xì)的分塊的好處了。當(dāng)用戶第一次訪問我們的 PWA,假設(shè)說他們?nèi)サ揭粋€(gè)特定的路由。服務(wù)器(使用 H/2 Push)能夠推送下來僅僅那次路由需要的分塊 —— 這些是用來啟動(dòng)應(yīng)用的必要資源,并會(huì)進(jìn)入網(wǎng)絡(luò)緩存中。
一旦它們被推送下來了,我們就能高效地準(zhǔn)備好未來會(huì)被加載的頁(yè)面分塊到緩存中。當(dāng)應(yīng)用啟動(dòng)后,檢查路由并指導(dǎo)我們想要的已經(jīng)在緩存中了,所以我們就能使得應(yīng)用的首次加載非???—— 不僅僅是閃屏 —— 而是用戶請(qǐng)求的可交互內(nèi)容。
下一部分是盡快渲染這個(gè)視圖的內(nèi)容。第三部分是,當(dāng)用戶在看當(dāng)前的視圖的時(shí)候,使用 Service Worker 來開始預(yù)緩存所有其他用戶還沒有請(qǐng)求的分塊和路由,將它們安裝到 Service Worker 的緩存中。
此時(shí),整個(gè)應(yīng)用(或者大部分)都已經(jīng)可以離線使用了。當(dāng)用戶導(dǎo)航到應(yīng)用的不同部分,我們可以從 Service Worker 的緩存中懶加載下面的部分。不需要網(wǎng)絡(luò)加載 —— 因?yàn)樗鼈円呀?jīng)被預(yù)緩存了。瞬間加載碉堡了!?
PRPL 可以被應(yīng)用到任何 app,正如 Flipkart 最近在他們的 React 棧上所展示的。完全使用 PRPL 的 Apps 可以利用 HTTP/2 服務(wù)器推送的快速加載,通過產(chǎn)生兩種編譯版本,并根據(jù)瀏覽器的支持提供不同版本:
一個(gè) bundled 編譯,為沒有 HTTP/2 推送支持的服務(wù)器/瀏覽器優(yōu)化以最小化往返。For most of us, this is what we ship today by default.
一個(gè)沒有 bundled 編譯,用于支持 HTTP/2 推送的服務(wù)器/瀏覽器,使得首次繪制更快。
這個(gè)部分基于我們?cè)谥坝懻摰穆酚煞謮K的想法。通過 PRPL,服務(wù)器和我們的 Service Worker 協(xié)作來為非活動(dòng)路由預(yù)緩存資源。當(dāng)一個(gè)用戶在你的 app 中瀏覽并改變路由,我們對(duì)尚未緩存的路由進(jìn)行懶加載,并創(chuàng)建請(qǐng)求的視圖。
實(shí)現(xiàn) PRPL
太長(zhǎng)了,所以沒有看:Webpack 的 require.ensure() 以及異步的 ‘getComponent’,還有 React Router 是到 PRPL 風(fēng)格性能模式的最小摩擦路徑
PRPL 的一大部分在于將 JS 捆包思維方式放下,并像編寫時(shí)候那樣精細(xì)地傳輸資源(至少?gòu)墓δ塥?dú)立模塊角度上)。通過 Webpack,這就是我們已經(jīng)說過的路由分塊。
對(duì)于初始路由推送關(guān)鍵資源。理想情況下,使用 HTTP/2 服務(wù)端推送,但即便沒有它,也不會(huì)成為實(shí)現(xiàn)類 PRPL 路徑的阻礙。即便沒有 H/2 推送,你也可以實(shí)現(xiàn)一個(gè)大致和“完整” PRPL 類似的結(jié)果,只需要發(fā)送 預(yù)加載頭 而不需要 H/2。
看看 Flipkart 他們前后的生產(chǎn)瀑布:
Webpack 已經(jīng)通過 AggressiveSplittingPlugin 的形式支持了 H/2。
AggressiveSplittingPlugin 分割每個(gè)塊直到它到達(dá)了指定的 maxSize,正如我們?cè)谙旅娴睦永锟梢姷模?/p>
module.exports = {
entry: "./example",
output: {
path: path.join(__dirname, "js"),
filename: "[chunkhash].js",
chunkFilename: "[chunkhash].js"
},
plugins: [
new webpack.optimize.AggressiveSplittingPlugin({
minSize: 30000,
maxSize: 50000
}),
// ...
查看官方 plugin page,以獲得關(guān)于更多細(xì)節(jié)的例子。學(xué)習(xí) HTTP/2 推送實(shí)驗(yàn)的課程 和 真實(shí)世界 HTTP/2 也值得一讀。
- 渲染初始路由:這實(shí)在取決于你使用的框架和庫(kù)。
- 預(yù)緩存剩下的路由。對(duì)于緩存,我們依賴于 Service Worker。sw-precache 對(duì)于生成用于靜態(tài)資源預(yù)緩存的 Service Worker 很棒,對(duì)于 Webpack 我們可以使用 SWPrecacheWebpackPlugin。
- 按需懶加載并創(chuàng)建剩下的路由 —— 在 Webpack 領(lǐng)域,require.ensure() 和 System.import() 是你的朋友。
通過 Webpack 的 Cache-busting 和長(zhǎng)期緩存
為什么關(guān)心靜態(tài)資源版本?
靜態(tài)資源指的是我們頁(yè)面中像是腳本,stylesheets 和圖片這樣的資源。當(dāng)用戶第一次訪問我們頁(yè)面的時(shí)候,他們需要其需要的所有資源。比如說當(dāng)我們落到一個(gè)路由的時(shí)候,JavaScript 塊和上次訪問之際并沒有改變 —— 我們不必重新抓取這些腳本因?yàn)樗麄円呀?jīng)在瀏覽器緩存中存在了。更少的網(wǎng)絡(luò)請(qǐng)求對(duì) web 性能來說是收益。
通常地,我們使用對(duì)每個(gè)文件設(shè)置 expires 頭 來達(dá)到目的。一個(gè) expires 頭只意味著我們可以告訴瀏覽器,避免在指定時(shí)間內(nèi)(比如說1年)發(fā)起另一個(gè)對(duì)該文件的請(qǐng)求到服務(wù)器。隨著代碼演變和重新部署,我們想要確保用戶可以獲得最新的文件,如果沒有改變的話則不需要重新下載資源。
Cache-busting 通過在文件名后面附加字符串來完成這個(gè) —— 他可以是一個(gè)編譯版本(比如 src=”chunk.js?v=1.2.0”),一個(gè) timestamp 或者別的什么。我傾向于添加一個(gè)文件內(nèi)容的 hash 到文件名(比如 chunk.d9834554decb6a8j.js)因?yàn)檫@個(gè)在文件內(nèi)容發(fā)生改變的時(shí)候總是會(huì)改變。MD5 hashing 在 Webpack 社區(qū)經(jīng)常被用來做這個(gè)來生成 16 字節(jié)長(zhǎng)的 ‘概要’。
通過 Webpack 的靜態(tài)資源長(zhǎng)期緩存 是關(guān)于這個(gè)主題的優(yōu)秀讀物,你應(yīng)該去看一看。我試圖在下面涵蓋其涉及到的主要內(nèi)容。
在 Webpack 中通過內(nèi)容哈希來做資源版本
在 Webpack 設(shè)置中加上如下內(nèi)容來啟用基于內(nèi)容哈希的資源版本 [chunkhash]:
filename: ‘[name].[chunkhash].js’,
chunkFilename: ‘[name].[chunkhash].js’
我們也想要保證常規(guī)的 [name].js 和 內(nèi)容哈希 ([name].[chunkhash].js) 文件名在我們的 HTML 文件被正確引用。不同之處在于引用 <script src=”chunk”.js”> 和 <script src=”chunk.d9834554decb6a8j.js”>。
下面是一個(gè)注釋了的 Webpack 設(shè)置樣例,包括了一些其他的插件來使得長(zhǎng)期緩存的安裝更優(yōu)雅。
const path = require('path');
const webpack = require('webpack');
// 使用 webpack-manifest-plugin 來生成包含了源文件到對(duì)應(yīng)輸出的映射的資源 manifest。Webpack 使用 IDs 而不是模塊名來保持生成的文件盡量小。IDs 在它們被放進(jìn) chunk manifest 之前被生成并映射到 chunk 的文件名(會(huì)跑到我們的入口 chunk)。不幸的是,任何對(duì)代碼的改變都會(huì)更新入口 chunk 包括新的 manifest,并刷新我們的緩存。
const ManifestPlugin = require('webpack-manifest-plugin');
// 我們通過 chunk-manifest-webpack-plugin 來修復(fù)這個(gè)問題,它會(huì)將 manifest 放到一個(gè)完全獨(dú)立的 JSON 文件。
const ChunkManifestPlugin = require('chunk-manifest-webpack-plugin');
module.exports = {
entry: {
vendor: './src/vendor.js',
main: './src/index.js'
},
output: {
path: path.join(__dirname, 'build'),
filename: '[name].[chunkhash].js',
chunkFilename: '[name].[chunkhash].js'
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: "vendor",
minChunks: Infinity,
}),
new ManifestPlugin(),
new ChunkManifestPlugin({
filename: "chunk-manifest.json",
manifestVariable: "webpackManifest"
}),
// 對(duì)非確定的模塊順序的權(quán)宜之計(jì)。在通過 Webpack 的靜態(tài)資源長(zhǎng)期緩存文章中有更多介紹
new webpack.optimize.OccurenceOrderPlugin()
]
};
現(xiàn)在我們有了這個(gè) chunk-manifest JSON 的編譯,我們需要把它內(nèi)聯(lián)(inline)到我們的 HTML,那么 Webpack 就能實(shí)際在頁(yè)面啟動(dòng)時(shí)真正對(duì)其有訪問權(quán)。所以在 <script> 標(biāo)簽中加上上面的輸出。
通過使用 html-webpack-plugin 可以實(shí)現(xiàn)自動(dòng)將腳本內(nèi)聯(lián)到 HTML 中。
注意:Webpack 理想上可以通過 no shared ID range 來簡(jiǎn)化啟用長(zhǎng)期緩存的步驟(見~4–1)。
如果要學(xué)習(xí)更多 HTTP 的 緩存最佳實(shí)踐,可以閱讀 Jake Archibald 的優(yōu)秀文章。
更多閱讀
- Webpack 關(guān)于代碼分割的文檔
- Formidable 的關(guān)于 Webpack 的 OSS Playbook 代碼分割 and shared libraries
- 使用 Webpack 的漸進(jìn)式 Web Apps
- 高級(jí) Webpack Part 2?—?代碼分割
- 為現(xiàn)代 web 應(yīng)用程序通過代碼分割來漸進(jìn)加載
- 在 React 組件中異步加載依賴
- 我們繼續(xù)前進(jìn)在 Webpack 插件 DLL
- 自動(dòng)代碼分割用于 React Router 和 ES6 Imports?—?Modus Create
- 使用 webpack 和 react-router 于懶加載和代碼分割沒有去加載
- 在現(xiàn)實(shí)生活通過 React 同構(gòu)/通用渲染/路由/數(shù)據(jù)抓取
- 一個(gè)懶得同構(gòu) React 實(shí)驗(yàn)
- 服務(wù)端渲染懶路由 基于 React Router 和代碼分割
- 給初學(xué)者的 React 在服務(wù)端?—?構(gòu)建一個(gè)通用的 React app
- 有頁(yè)面的 React.js Apps
- 將世界銀行數(shù)據(jù)網(wǎng)站構(gòu)建為使用代碼分割的快速加載單頁(yè)應(yīng)用
- 在 Gatsby 實(shí)現(xiàn) PRPL(React.js 靜態(tài)網(wǎng)站生成器)
高級(jí)模塊打包優(yōu)化讀物
在系列文章第三篇中,我們會(huì)來看看 怎么使你的 React PWA 能離線和斷續(xù)的網(wǎng)絡(luò)狀態(tài)下工作.
如果你新接觸 React,我發(fā)現(xiàn) Wes Bos 寫的 給新手的 React 很棒。
感謝 Gray Norton, Sean Larkin, Sunil Pai, Max Stoiber, Simon Boudrias, Kyle Mathews 和 Owen Campbell-Moore 的校對(duì)。