系列第二篇姨伟,來看看基于 React 路由分塊的頁面加載優(yōu)化污秆。
- 原文作者:Addy Osmani
- 譯文出自:掘金翻譯計劃
- 譯者:markzhai
使用 React.js 的漸進式 Web 應(yīng)用程序:第 2 部分 - 頁面加載性能
新 系列 的第二部分會完整地走一遍怎么使用 Lighthouse. 來優(yōu)化移動 web apps疼电。這篇文章院喜,我們來看看頁面加載性能爷辱。
保證頁面加載性能是快的
移動 Web 的速度很關(guān)鍵既鞠。平均地煤傍,更快的體驗會帶來 70% 更長的會話 以及兩倍以上更多的移動廣告收益。Web 性能的投資像是基于 React 的 Flipkart Lite 獲得了三倍網(wǎng)站瀏覽時間嘱蛋, GQ 在流量上得到了 80% 增長蚯姆,Trainline 在 年收益上增長了 11M 并且 Instagram 增長了 33% 的印象.
在你的 web app 加載時有一些 關(guān)鍵的用戶時刻:
測量然后優(yōu)化總是關(guān)鍵的五续。Lighthouse 的頁面加載檢測會關(guān)注:
- 第一次有意義的繪制(當頁面主內(nèi)容可見)
- 速度指數(shù)(Speed Index)(可見區(qū)域完整)
- 估算的輸入延遲(當主線程可以立即處理用戶輸入)
- 以及 抵達可交互的時間(app 多快達到可用可參與)
順帶一提,Paul Irish 做了很了不起的相關(guān)總結(jié) PWAs 的有趣指標值得一看龄恋。
良好性能的目標:
- 遵循 RAIL 性能模型 的 L 部分疙驾。** A+ 的性能是我們所有人都必須力求達到的,即便有的瀏覽器不支持 Service Worker郭毕。我們?nèi)匀豢梢钥焖俚卦谄聊簧汐@得一些有意義的內(nèi)容荆萤,并且僅加載我們所需要的**
- 在典型網(wǎng)絡(luò)(3G)和硬件條件下
- 首次訪問在 5 秒內(nèi)可交互,重復(fù)訪問(Service Worker 可用)則在 2 秒內(nèi)铣卡。
- 首次加載(網(wǎng)絡(luò)限制下)链韭,速度指數(shù)在 3000 或者更少。
- 第二次加載(磁盤限制煮落,因為 Service Worker 可用):速度指數(shù) 1000 或者更少敞峭。
讓我們再說說,關(guān)于通過 TTI 關(guān)注交互性蝉仇。
關(guān)注抵達可交互時間(TTI)
為交互性優(yōu)化旋讹,也就是使得 app 盡快能對用戶可用(比如讓他們可以四處點擊,app 可以相應(yīng))轿衔。這對試圖在移動設(shè)備上提供一流用戶體驗的現(xiàn)代 web 體驗很關(guān)鍵沉迹。
Lighthouse 現(xiàn)在將 TTI 測量為布局穩(wěn)定,web 字體可見害驹,并且主線程可以響應(yīng)用戶輸入的時間鞭呕。有很多方法來手動跟蹤 TTI,重要的是根據(jù)指標進行優(yōu)化會提升你用戶的體驗宛官。
對于像 React 這樣的庫葫松,你應(yīng)該關(guān)心的是在移動設(shè)備上 啟用庫的代價 因為這會讓人們有感知。在 ReactHN底洗,我們達到了 1700毫秒 內(nèi)可交互腋么,通過保持整個 app 的大小和執(zhí)行代價相對小,撇開有多個視圖:app bundle gzipped 壓縮后 11KB亥揖,107KB 用于我們的 vendor/React/庫 bundle珊擂,實踐中有點像這樣:
之后,對于功能顆粒狀的 apps费变,我們會看看性能模式像是 PRPL摧扇,通過在 HTTP/2 服務(wù)器 Push 下利用顆粒狀的 “基于路由的分塊” 來得到快速的可交互時間。(可以試試 Shop demo 來看看我們說的是什么)胡控。
Housing.com 最近使用了類 PRPL 模式搭載 React 體驗扳剿,獲得了很多贊揚:
Housing.com 利用 Webpack 路由分塊,來推遲入口頁面的部分啟動消耗(僅加載 route 渲染所需要的)昼激。更多細節(jié)請查看 Sam Saccone 的優(yōu)秀 Housing.com 性能檢測.
Flipkart 也做了類似的:
注意:關(guān)于什么是 “到可交互時間”庇绽,有很多不同的看法锡搜,Lighthouse 對 TTI 的定義也可能會演變。其他跟蹤的方法有導(dǎo)航后第一個 5 秒內(nèi) window 沒有長任務(wù)的時刻瞧掺,或者一次文本/內(nèi)容繪制后第一次 5 秒內(nèi) window 沒有長任務(wù)的時刻耕餐。基本上辟狈,就是頁面穩(wěn)定后多快肠缔,用戶可以和 app 交互的。
注意:盡管不是強制的要求哼转,你可能也需要提高視覺完整度(速度指數(shù))明未,通過 優(yōu)化關(guān)鍵渲染路徑。關(guān)鍵路徑 CSS 優(yōu)化工具的存在 以及其優(yōu)化在 HTTP/2 的世界中依然有效壹蔓。
基于路由分塊以提高性能
Webpack
如果你第一次接觸模塊打包工具像是 Webpack趟妥,看看 JS 模塊化打包器(視頻) 可能會有幫助。
一些今天的 JavaScript 工具使得將你的所有腳本打包成一個 bundle.js 文件并包含所有頁面變得簡單佣蓉。這意味著很多時候披摄,你可能要加載很多對當前路由來說并不需要的代碼。為什么一次路由需要加載 500KB 的 JS勇凭,而事實上 50KB 就夠了呢疚膊?我們應(yīng)該丟開那些無助于獲得更快體驗的腳本,來加速獲得可交互的路由虾标。
當僅提供用戶一次 route 所需要的最小功能可達代碼就可以的時候寓盗,避免提供龐大整塊的 bundles(像上圖)。
代碼分割是解決整塊的 bundles 的一個方法夺巩。想法大致是在你的代碼中定義分割點贞让,然后分割成不同的文件進行按需懶加載。這會提升啟動時間柳譬,幫助我們更快地可交互。
想象使用一個公寓列表 app续镇。如果我們登陸的路由是列出我們所在區(qū)域的地產(chǎn)(route-1)—— 我們不需要查看完整的地產(chǎn)詳情的代碼(route-2)或者預(yù)約一次看房(route-3)美澳,所以我們可以僅提供用戶列表路由所需要的 JavaScript,然后動態(tài)加載剩下的摸航。
這些年來制跟,代碼分割的想法已經(jīng)被很多 apps 使用,但現(xiàn)在用 “基于路由的分塊” 來稱呼它酱虎。通過 Webpack 模塊打包器雨膨,我們可以啟用 React 上的安裝。
實踐基于路由的代碼分塊
Webpack 支持當它發(fā)現(xiàn)一個 require.ensure() 被使用的時候?qū)⒛愕?app 代碼分割成塊(或者在 Webpack 2读串,一個 System.import)聊记。這些被稱為 “分割點”撒妈,Webpack 會對它們的每一個都生成一個分開的 bundlea,按需解決依賴排监。
// 定義一個 "split-point"
require.ensure([], function () {
const details = require('./Details');
// 所有被 require() 需要的都會成為分開的 bundle
// require(deps, cb) 是異步的狰右。它會異步加載,并且評估
// 模塊舆床,通過你的 deps 的 exports 調(diào)用 cb棋蚌。
});
當你的代碼需要某些東西,Webpack 會發(fā)起一個 JSONP 請求來從服務(wù)器獲得它挨队。這個和 React Router 結(jié)合工作得很好谷暮,我們可以在對用戶渲染視圖之前在依賴(塊)中懶加載一個新的路由。
Webpack 2 支持 使用 React Router 的自動代碼分割 因為它可以處理模塊上的 System.import 調(diào)用為 import 語句盛垦,將導(dǎo)入的文件和它們的依賴一起打包坷备。依賴不會與你在 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ù)之前省撑,除了剛才的方法,另一個可選的是 來自 Resource Hints俯在。這個提供給我們一個方法來宣告式地獲取資源而不執(zhí)行它們竟秫。預(yù)加載可以用來預(yù)加載用戶可能要去的路由所需要的 Webpack 塊,于是緩存已經(jīng)為他們準備好了跷乐,可以在需要的時候立即可用肥败。
在寫的時候,預(yù)加載只能在 Chrome 中進行愕提,但是在支持的瀏覽器中被處理為漸進式增加馒稍。
注意:html-webpack-plugin 的 模板和自定義事件 可以使用最小的改變來讓簡化這個過程。然后你應(yīng)該保證預(yù)加載的資源真正會對你大部分的用戶瀏覽過程有用浅侨。
異步加載路由
讓我們回到代碼分割(code-splitting)—— 在一個使用 React 和 React Router 的 app 里纽谒,我們可以使用 require.ensure() 以在 ensure 被調(diào)用的時候異步加載一個組件。順帶一提如输,如果任何人在探索服務(wù)器渲染鼓黔,這個在 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)來進行上面的安裝缎谷,所以在我不同的入口點之間有一個帶有通用模塊的 chunk。這也 極力降低了 陷入缺省 Webpack runtime 的可能灶似。
Brian Holt 在 React 的完整介紹 對異步路由加載涵蓋得很好列林。通過異步路由的代碼分割在 React Router 的目前版本和 新的 React Router V4 都可以使用瑞你。
通過異步的 getComponent + require.ensure() 的簡單宣告式路由分塊
這有有一個小貼士,可以使代碼分割的安裝更快席纽。在 React Router捏悬,一個 宣告式的路由 來將路由 “/” 映射到組件 App
看上去像 <Route path=”/” component={App}>
.
React Router 也支持一個方便的 [getComponent](https://github.com/ReactTraining/react-router/blob/master/docs/API.md#getcomponentnextstate-callback)
屬性,類似于 component
但卻是異步的润梯,對快速安裝上代碼分割超級有用:
<Route
path="stories/:storyId"
getComponent={(nextState, cb) => {
// 異步地查找 components
cb(null, Stories)
}} />
getComponent
函數(shù)參數(shù)包括下一個狀態(tài)(我設(shè)置為 null)和一個回調(diào)过牙。
讓我們添加一些基于路由的代碼分割到 ReactHN。我們會從 routes 文件中的一段開始?——?它定義了組件的 require 調(diào)用和對每個路由的 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)在提供給用戶一個整塊包含所有路由的 JS bundle纺铭。讓我們將它轉(zhuǎn)換為路由分塊寇钉,只提供一次路由真正需要的代碼,從 comment 的永久鏈接開始(comment/:id):
所以我們首先刪了對永久鏈接組件的隱式 require:
var PermalinkedComment = require(‘./PermalinkedComment’)
然后開始我們的路由..
然后使用宣告式的 getComponent 來更新它舶赔。我們在路由中使用 require.ensure() 調(diào)用來懶加載宏胯,而這就是我們所需要做的一切了:
<Route
path="comment/:id"
getComponent={(location, callback) => {
require.ensure([], require => {
callback(null, require('./PermalinkedComment'))
}, 'PermalinkedComment')
}}
/>
Orz 太美了习绢。這..就搞定了鼠次。不騙你息罗。我們可以把這個用到剩下的路由上,然后運行 webpack锥累。它會正確地找到 require.ensure() 調(diào)用缘挑,然后如我們想要地區(qū)分隔代碼。
在應(yīng)用宣告式的代碼分割到更多我們的路由后桶略,可以看到路由分塊在運作语淘,僅僅會加載一次路由所需要的代碼(在 Service Worker 可以預(yù)緩存起來):
提醒:有許多可用于 Service Worker 的簡單 Webpack 插件:
- sw-precache-webpack-plugin 在底層使用 sw-precache
- offline-plugin 被 react-boilerplate 所使用
CommonsChunkPlugin
為了識別出在不同路由使用的通用模塊并把它們放在一個通用的分塊,需要使用 CommonsChunkPlugin际歼。它需要在每個頁面 requires 兩個 script 標簽惶翻,一個用于 commons 分塊,另一個用于一次路由的入口分塊鹅心。
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 標志 對于查看模塊在哪個分塊中出現(xiàn)很有用吕粗。這個幫助我們減少分塊中重復(fù)的依賴,并且能提示在你的項目中開啟 CommonChunksPlugin 是否值得巴帮。這是一個帶有多個組件的項目溯泣,在不同分塊間檢測到重復(fù)的 Mustache.js 依賴:
Webpack 1 也支持通過 DedupePlugin 以在你的依賴樹中進行依賴庫的去重。在 Webpack 2榕茧,tree-shaking 應(yīng)該淘汰了這個的需求。
更多 Webpack 的小貼士
- 你的代碼庫中 require.ensure() 調(diào)用的數(shù)目通常會關(guān)聯(lián)到生成的 bundles 的數(shù)目客给。在代碼庫中大量使用 ensure 的時候意識到這點很有用用押。
- Webpack2 的 Tree-shaking 會幫助刪除沒用的 exports,這可以讓你的 bundle 尺寸變小靶剑。
- 另外蜻拨,小心避免在 通用/共享的 bundles 里面調(diào)用 require.ensure()池充。你可能會發(fā)現(xiàn)這會創(chuàng)建入口點引用,關(guān)于那些已經(jīng)被加載了的依賴的假設(shè)缎讼。
- 在 Webpack 2收夸,System.import 目前不支持服務(wù)端渲染,但我已經(jīng)在 StackOverflow 分享了怎么去處理這個問題血崭。
- 如果需要優(yōu)化編譯速度卧惜,可以看看 Dll plugin,parallel-webpack 以及目標的編譯夹纫。
- 如果你希望通過 Webpack 異步 或者 延遲 腳本咽瓷,看看 script-ext-html-webpack-plugin
在 Webpack 編譯中檢測臃腫
Webpack 社區(qū)有很多建立在 Web 上的編譯分析器包括 http://webpack.github.io/analyse/,https://chrisbateman.github.io/webpack-visualizer/舰讹,和 https://alexkuz.github.io/stellar-webpack/茅姜。這些對于理解你最大的模塊是什么很有用。
source-map-explorer (來自 Paul Irish) 通過 source maps 來理解代碼臃腫月匣,也超級棒的钻洒。看看這個對 ReactHN Webpack bundle 的 tree-map 可視化锄开,帶有每個文件的代碼行數(shù)素标,以及百分比的統(tǒng)計分析:
你可能也會對來自 Sam Saccone 的 coverage-ext 感興趣,它可以用來對任何 webapp 生成代碼覆蓋率院刁。這個對于理解你的代碼中有多少實際會被執(zhí)行到很有用糯钙。
代碼分割(code-splitting)之上:PRPL 模式
Polymer 發(fā)現(xiàn)了一個有趣的 web 性能模式,用于精細服務(wù)的 apps退腥,稱為 PRPL(看看 Kevin 的 I/O 演講)任岸。這個模式嘗試為交互性優(yōu)化,并且代表了:
- (P)ush狡刘,對于初始路由推送關(guān)鍵資源享潜。
- (R)ender,渲染初始路由嗅蔬,并使它盡快變得可交互剑按。
- (P)re-cache,通過 Service Worker 預(yù)緩存剩下的路由澜术。
- (L)azy-load艺蝴,根據(jù)用戶在應(yīng)用中的移動懶加載并懶初始化 apps 中對應(yīng)的部分。
在這里鸟废,我們必須給予 Polymer Shop demo 大大的贊賞猜敢,因為它向我們展示了真實移動設(shè)備上的道路。使用 PRPL(在這種情況下通過 HTML Imports,從而利用瀏覽器的后臺 HTML parser 的好處)缩擂。屏幕上的像素你都可以使用鼠冕。這里額外的工作在于分塊和保持可交互。在一臺真實移動設(shè)備上胯盯,我們可以在 1.75 秒內(nèi)達到可交互懈费。1.3 秒用于 JavaScript,但它都被打散了博脑。在那以后所有功能都可以用了憎乙。
你到現(xiàn)在應(yīng)該已經(jīng)成功享受到講應(yīng)用打碎到更精細的分塊的好處了。當用戶第一次訪問我們的 PWA趋厉,假設(shè)說他們?nèi)サ揭粋€特定的路由寨闹。服務(wù)器(使用 H/2 Push)能夠推送下來僅僅那次路由需要的分塊 —— 這些是用來啟動應(yīng)用的必要資源,并會進入網(wǎng)絡(luò)緩存中君账。
一旦它們被推送下來了繁堡,我們就能高效地準備好未來會被加載的頁面分塊到緩存中。當應(yīng)用啟動后乡数,檢查路由并指導(dǎo)我們想要的已經(jīng)在緩存中了椭蹄,所以我們就能使得應(yīng)用的首次加載非常快 —— 不僅僅是閃屏 —— 而是用戶請求的可交互內(nèi)容净赴。
下一部分是盡快渲染這個視圖的內(nèi)容绳矩。第三部分是,當用戶在看當前的視圖的時候玖翅,使用 Service Worker 來開始預(yù)緩存所有其他用戶還沒有請求的分塊和路由翼馆,將它們安裝到 Service Worker 的緩存中。
此時金度,整個應(yīng)用(或者大部分)都已經(jīng)可以離線使用了应媚。當用戶導(dǎo)航到應(yīng)用的不同部分,我們可以從 Service Worker 的緩存中懶加載下面的部分猜极。不需要網(wǎng)絡(luò)加載 —— 因為它們已經(jīng)被預(yù)緩存了中姜。瞬間加載碉堡了!?
PRPL 可以被應(yīng)用到任何 app跟伏,正如 Flipkart 最近在他們的 React 棧上所展示的丢胚。完全使用 PRPL 的 Apps 可以利用 HTTP/2 服務(wù)器推送的快速加載,通過產(chǎn)生兩種編譯版本受扳,并根據(jù)瀏覽器的支持提供不同版本:
一個 bundled 編譯携龟,為沒有 HTTP/2 推送支持的服務(wù)器/瀏覽器優(yōu)化以最小化往返。For most of us, this is what we ship today by default.
一個沒有 bundled 編譯勘高,用于支持 HTTP/2 推送的服務(wù)器/瀏覽器骨宠,使得首次繪制更快浮定。
這個部分基于我們在之前討論的路由分塊的想法相满。通過 PRPL层亿,服務(wù)器和我們的 Service Worker 協(xié)作來為非活動路由預(yù)緩存資源。當一個用戶在你的 app 中瀏覽并改變路由立美,我們對尚未緩存的路由進行懶加載匿又,并創(chuàng)建請求的視圖。
實現(xiàn) PRPL
太長了建蹄,所以沒有看:Webpack 的 require.ensure() 以及異步的 ‘getComponent’碌更,還有 React Router 是到 PRPL 風格性能模式的最小摩擦路徑
PRPL 的一大部分在于將 JS 捆包思維方式放下,并像編寫時候那樣精細地傳輸資源(至少從功能獨立模塊角度上)洞慎。通過 Webpack痛单,這就是我們已經(jīng)說過的路由分塊。
對于初始路由推送關(guān)鍵資源旭绒。理想情況下焦人,使用 HTTP/2 服務(wù)端推送挥吵,但即便沒有它花椭,也不會成為實現(xiàn)類 PRPL 路徑的阻礙忽匈。即便沒有 H/2 推送,你也可以實現(xiàn)一個大致和“完整” PRPL 類似的結(jié)果丹允,只需要發(fā)送 預(yù)加載頭 而不需要 H/2袋倔。
看看 Flipkart 他們前后的生產(chǎn)瀑布:
Webpack 已經(jīng)通過 AggressiveSplittingPlugin 的形式支持了 H/2雕蔽。
AggressiveSplittingPlugin 分割每個塊直到它到達了指定的 maxSize,正如我們在下面的例子里可見的:
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)于更多細節(jié)的例子。學(xué)習 HTTP/2 推送實驗的課程 和 真實世界 HTTP/2 也值得一讀碳默。
- 渲染初始路由:這實在取決于你使用的框架和庫。
- 預(yù)緩存剩下的路由髓废。對于緩存该抒,我們依賴于 Service Worker慌洪。sw-precache 對于生成用于靜態(tài)資源預(yù)緩存的 Service Worker 很棒,對于 Webpack 我們可以使用 SWPrecacheWebpackPlugin涌攻。
- 按需懶加載并創(chuàng)建剩下的路由 —— 在 Webpack 領(lǐng)域,require.ensure() 和 System.import() 是你的朋友恳谎。
通過 Webpack 的 Cache-busting 和長期緩存
為什么關(guān)心靜態(tài)資源版本憋肖?
靜態(tài)資源指的是我們頁面中像是腳本,stylesheets 和圖片這樣的資源岸更。當用戶第一次訪問我們頁面的時候,他們需要其需要的所有資源怎炊。比如說當我們落到一個路由的時候,JavaScript 塊和上次訪問之際并沒有改變 —— 我們不必重新抓取這些腳本因為他們已經(jīng)在瀏覽器緩存中存在了赞咙。更少的網(wǎng)絡(luò)請求對 web 性能來說是收益糟港。
通常地,我們使用對每個文件設(shè)置 expires 頭 來達到目的秸抚。一個 expires 頭只意味著我們可以告訴瀏覽器,避免在指定時間內(nèi)(比如說1年)發(fā)起另一個對該文件的請求到服務(wù)器剥汤。隨著代碼演變和重新部署,我們想要確保用戶可以獲得最新的文件碰凶,如果沒有改變的話則不需要重新下載資源。
Cache-busting 通過在文件名后面附加字符串來完成這個 —— 他可以是一個編譯版本(比如 src=”chunk.js?v=1.2.0”)欲低,一個 timestamp 或者別的什么畜晰。我傾向于添加一個文件內(nèi)容的 hash 到文件名(比如 chunk.d9834554decb6a8j.js)因為這個在文件內(nèi)容發(fā)生改變的時候總是會改變。MD5 hashing 在 Webpack 社區(qū)經(jīng)常被用來做這個來生成 16 字節(jié)長的 ‘概要’凄鼻。
通過 Webpack 的靜態(tài)資源長期緩存 是關(guān)于這個主題的優(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”>
。
下面是一個注釋了的 Webpack 設(shè)置樣例,包括了一些其他的插件來使得長期緩存的安裝更優(yōu)雅其徙。
const path = require('path');
const webpack = require('webpack');
// 使用 webpack-manifest-plugin 來生成包含了源文件到對應(yīng)輸出的映射的資源 manifest。Webpack 使用 IDs 而不是模塊名來保持生成的文件盡量小访锻。IDs 在它們被放進 chunk manifest 之前被生成并映射到 chunk 的文件名(會跑到我們的入口 chunk)闹获。不幸的是期犬,任何對代碼的改變都會更新入口 chunk 包括新的 manifest避诽,并刷新我們的緩存。
const ManifestPlugin = require('webpack-manifest-plugin');
// 我們通過 chunk-manifest-webpack-plugin 來修復(fù)這個問題鲤妥,它會將 manifest 放到一個完全獨立的 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"
}),
// 對非確定的模塊順序的權(quán)宜之計棉安。在通過 Webpack 的靜態(tài)資源長期緩存文章中有更多介紹
new webpack.optimize.OccurenceOrderPlugin()
]
};
現(xiàn)在我們有了這個 chunk-manifest JSON 的編譯铸抑,我們需要把它內(nèi)聯(lián)(inline)到我們的 HTML,那么 Webpack 就能實際在頁面啟動時真正對其有訪問權(quán)鹊汛。所以在 <script>
標簽中加上上面的輸出。
通過使用 html-webpack-plugin 可以實現(xiàn)自動將腳本內(nèi)聯(lián)到 HTML 中凳宙。
注意:Webpack 理想上可以通過 no shared ID range 來簡化啟用長期緩存的步驟(見~4–1)职祷。
如果要學(xué)習更多 HTTP 的 緩存最佳實踐氏涩,可以閱讀 Jake Archibald 的優(yōu)秀文章。
更多閱讀
- Webpack 關(guān)于代碼分割的文檔
- Formidable 的關(guān)于 Webpack 的 OSS Playbook 代碼分割 and shared libraries
- 使用 Webpack 的漸進式 Web Apps
- 高級 Webpack Part 2?—?代碼分割
- 為現(xiàn)代 web 應(yīng)用程序通過代碼分割來漸進加載
- 在 React 組件中異步加載依賴
- 我們繼續(xù)前進在 Webpack 插件 DLL
- 自動代碼分割用于 React Router 和 ES6 Imports?—?Modus Create
- 使用 webpack 和 react-router 于懶加載和代碼分割沒有去加載
- 在現(xiàn)實生活通過 React 同構(gòu)/通用渲染/路由/數(shù)據(jù)抓取
- 一個懶得同構(gòu) React 實驗
- 服務(wù)端渲染懶路由 基于 React Router 和代碼分割
- 給初學(xué)者的 React 在服務(wù)端?—?構(gòu)建一個通用的 React app
- 有頁面的 React.js Apps
- 將世界銀行數(shù)據(jù)網(wǎng)站構(gòu)建為使用代碼分割的快速加載單頁應(yīng)用
- 在 Gatsby 實現(xiàn) PRPL(React.js 靜態(tài)網(wǎng)站生成器)
高級模塊打包優(yōu)化讀物
在系列文章第三篇中意系,我們會來看看 怎么使你的 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 的校對迎瞧。