前言
對於頁面中靜態資源(html/js/css/img/webfont),理想中的效果:
-
頁面以最快的速度獲取到全部必須靜態資源,渲染飛快;
-
服務器上靜態資源未更新時再次訪問不請求服務器;
-
服務器上靜態資源更新時請求服務器最新資源,加載又飛快。
靜態資源加載速度引出了咱們今天的主題,由於最直接的方式就是將靜態資源進行緩存。頁面渲染速度創建在資源加載速度之上,但不一樣資源類型的加載順序和時機也會對其產生影響,因此也留給了咱們更多的優化空間。
固然除了速度,緩存還有另外2大功效,減小用戶請求的帶寬和減小服務器壓力。
常見緩存類型
對於前端而言,這多是咱們最容易忽略的緩存類型,緣由在於大部分設置都在服務器運維層面上進行,不屬於前端開發的維護範圍。但靜態資源的內容更新時機其實前端是最清楚的,若是能在理解瀏覽器緩存策略的基礎上合理配置效果最佳。
瀏覽器緩存策略通常經過資源的Response Header來定義,html文件在很早以前的規範裏也能夠經過Meta標籤的http-equiv來定義。
可在w3c的官方文檔中查看全部HTTP Response Header字段的定義,跟緩存相關的主要有上圖中被圈出來的幾個:前端
-
- public:響應被緩存,而且在多用戶間共享。
- private:默認值,響應只可以做爲私有的緩存(e.g., 在一個瀏覽器中),不能再用戶間共享;
- no-cache:響應不會被緩存,而是實時向服務器端請求資源。
- max-age:數值,單位是秒,從請求時間開始到過時時間之間的秒數。基於請求時間(Date字段)的相對時間間隔,而不是絕對過時時間;
注:HTTP/1.0 沒有實現 Cache-Control,因此爲了兼容HTTP/1.0出現了Pragma字段。
-
Pragma: 只有一個用法Pragma: no-cache,它和Cache-Control:no-cache做用如出一轍。(Cache-Control: no-cache是http 1.1才提供的, 所以Pragma: no-cache可使no-cache應用到http 1.0 和http 1.1。)
-
Expires:指定了在瀏覽器上緩衝存儲的頁距過時還有多少時間,等同Cache-control中的max-age的效果,若是同時存在,則被Cache-Control的max-age覆蓋。若把其值設置爲0,則表示頁面當即過時。而且若此屬性在頁面當中被設置了屢次,則取其最小值。
注:這個規則容許源服務器,對於一個給定響應,向 HTTP/1.1(或以後)緩存比 HTTP/1.0 提供一個更長的過時時間。
-
Date:生成消息的具體時間和日期;
-
Last-Modified/If-Modified-Since:本地文件在服務器上的最後一次修改時間。緩存過時時把瀏覽器端緩存頁面的最後修改時間發送到服務器去,服務器會把這個時間與服務器上實際文件的最後修改時間進行對比,若是時間一致,那麼返回304,客戶端就直接使用本地緩存文件。
-
Etag/If-None-Match:(EntityTags)是URL的tag,用來標示URL對象是否改變,通常爲資源實體的哈希值。和Last-Modified相似,若是服務器驗證資源的ETag沒有改變(該資源沒有更新),將返回一個304狀態告訴客戶端使用本地緩存文件。Etag的優先級高於Last-Modified,Etag主要爲了解決 Last-Modified 沒法解決的一些問題。
- 文件也許會週期性的更改,可是他的內容並不改變,不但願客戶端從新get;
- If-Modified-Since能檢查到的粒度是s級;
- 某些服務器不能精確的獲得文件的最後修改時間。
本地緩存過時後,瀏覽器會像服務器發送請求,request中會攜帶如下兩個字段:
其中在圖右側的「file modified?」判斷中,服務器會讀取請求頭這兩個值,判斷出客戶端緩存的資源是否最新,若是是的話服務器就會返回HTTP/304 Not Modified響應頭,但沒有響應體。客戶端收到304響應後,就會從緩存中讀取對應的資源;不然返回HTTP/200和響應體。
meta是html語言head區的一個輔助性標籤,其中的http-equiv字段定義了服務器和用戶代理的一些行爲。在以前的規範中,meta的http-equiv字段中有如下值與http header緩存相關的字段功能相似。
-
Cache-Control
-
Pragma
-
Expires
<meta http-equiv="Cache-Control" content="no-cache" /> <!-- HTTP 1.1 -->
<meta http-equiv="Pragma" content="no-cache" /> <!-- 兼容HTTP1.0 -->
<meta http-equiv="Expires" content="0" /> <!-- 資源到期時間設爲0 -->複製代碼
但如今
w3c的規範字段中這些值已經被移除,一個很好的理由是:
Putting caching instructions into meta tags is not a good idea, because although browsers may read them, proxies won't. For that reason, they are invalid and you should send caching instructions as real HTTP headers.
其實也很好理解,寫在meta標籤中表明必須解析讀取html的內容,但代理服務器是不會去讀取的。大多瀏覽器已經再也不支持,會忽略這樣的寫法,因此緩存仍是經過HTTP headers去設置。
注:HTTP Headers中的緩存設置優先級比meta中http-equiv更高一些。
二、HTML5 Application Cachehtml5
Application Cache是html5引入的本地存儲方案之一,能夠構建離線緩存。目前除IE10-外其餘瀏覽器均支持。
application cache是經過mannifest文件來管理的,manifest文件是簡單的文本文件,內容是須要被緩存供離線使用的文件列表,及不須要被緩存或讀取緩存失敗的文件控制。
mannifest文件可使用任意拓展名,但須要在服務器中添加MIME類型匹配,使用apache比較簡單,若是使用.manifest做爲拓展名在apache配置文件中添加。
AddType text/cache-manifest .appcache複製代碼
<html lang="zh" manifest="main.manifest">複製代碼
注:千萬不要把manifest文件自己放在緩存文件列表中,否則瀏覽器沒法更新manifest文件文件,最好在manifest文件的http headers中設置其當即過時。
-
Creating Application Cache with manifest(訪問到帶manifest屬性的html文件,將manifest文件存儲,加載html文件及其餘資源文件);
-
Application Cache Checking event(檢查要緩存的文件列表)
-
Application Cache Downloading event(開始下載緩存文件)
-
Application Cache Progress event (0 of 4)(依次下載緩存文件)
-
……
-
Application Cache Progress event (4 of 4)
-
Application Cache Cached event(文件緩存完畢)
-
Document was loaded from Application Cache with manifest(從緩存中讀取html文件和其餘靜態資源文件,供頁面展現)
-
Application Cache Checking event(獲取新的manifest文件,檢查是否更新)
-
Application Cache Obsolete event(刪除本地緩存中的全部文件,再也不使用緩存)
-
Application Cache會默認緩存引用manifest文件的HTML文檔,對於動態更新的html頁面來講是個坑(可使用tricky的iframe嵌入方式來避免);
-
只要緩存列表中的一個資源加載失敗,全部文件都將緩存失敗;
-
若是資源沒有被緩存,而又沒有設置NETWORK的狀況下,將會沒法加載,因此Network中必須使用通配符配置;
-
緩存更新後第一次只能加載manifest文件,其餘靜態資源須要第二次加載才能看到最新效果;
-
緩存文件清單中的文件自己更新瀏覽器是不會從新緩存,那怎麼告訴瀏覽器緩存須要更新了呢?
- 更新manifest文件:修改註釋的版本號或者日期。
- 經過Application Cache提供的接口(window.applicationCache.swapCache)來檢查更新。
還有最後一個問題,該標準已經從 Web 標準中刪除……
該特性已經從 Web 標準中刪除,雖然一些瀏覽器目前仍然支持它,但也許會在將來的某個時間中止支持,請儘可能不要使用該特性。在此刻使用這裏描述的應用程序緩存功能高度不鼓勵; 它正在處於從Web平臺中被刪除的過程。請改用
Service Workers 代替。
三、PWA(Service Worker)apache
PWA全稱爲「Progressive Web Apps」,漸進式網頁應用,Service Worker是其幾大核心技術之一。
Service worker is a programmable network proxy, allowing you to control how network requests from your page are handled.
沒錯,這就是官方建議替代Application Cache的方案。早在2014年,W3C就公佈了Service Worker的草案。它做爲一個獨立的線程,是一段在後臺運行的腳本。它的出現使得web app也能夠具備相似native app的離線使用、消息推送、後臺自動更新等能力。
一、首先,要使用Service Worker,須要添加一個Service Worker的js的文件,而後在咱們的html頁面中註冊對這個文件的引用。
<script>
navigator.serviceWorker
.register('./sw.js')
.then(function (registration) {
// 註冊成功
});
</script>複製代碼
二、其次,咱們在js文件中補充Service Worker的生命週期事件。Service Worker生命週期有三部曲:註冊,安裝和激活。
self.addEventListener('install', function(event) {
/* 安裝後... */
// cache.addAll:把緩存文件加進來,如a.css,b.js
});
self.addEventListener('activate', function(event) {
/* 激活後... */
// caches.delete :更新緩存文件
});
self.addEventListener('fetch', function(event) {
/* 請求資源後... */
// cache.put 攔截請求直接返回緩存數據
});複製代碼
對於獲取文件和緩存文件,Service worker依賴了兩個 API:
Fetch (經過網絡從新獲取內容的標準方式) 和
Cache(應用數據的內容存儲,此緩存獨立於瀏覽器緩存和網絡狀態)。
index.html文件中引用了static/js/main.js,main.js中註冊了service-worker.js。service-worker.js中咱們能夠看到有 precacheConfig(緩存列表)和 cacheName(版本號)兩個變量。斷開網絡,咱們看到precacheConfig列表中的文件仍能從本地加載。
以註冊文件爲service-worker.js爲例,每次訪問ServiceWorker控制的頁面,瀏覽器都會加載最新的service-worker.js文件,跟當前service-worker.js文件對比,只要內容有任何不一樣,瀏覽器都會獲取並安裝新文件。可是不會當即生效,原有的ServiceWorker仍是會運行,只有當ServiceWorker控制的頁面所有關閉後,新的ServiceWorker纔會被激活。
LocalStorage雖是瀏覽器端緩存一種,但有多少人會用它來緩存文件呢?首先緩存讀取須要依靠js的執行,因此前提條件就是可以讀取到html及js代碼段;其次文件的版本更新控制會帶來更多的代碼層面的維護成本,因此LocalStorage更適合關鍵的業務數據而非靜態資源。
這是一種以空間換時間的方案,減小了用戶的訪問延時,也減小的源站的負載。
客戶端瀏覽器先檢查是否有本地緩存是否過時,若是過時,則向CDN邊緣節點發起請求,CDN邊緣節點會檢測用戶請求數據的緩存是否過時,若是沒有過時,則直接響應用戶請求,此時一個完成HTTP請求結束;若是數據已通過期,那麼CDN還須要向源站發出回源請求。
CDN邊緣節點緩存策略因服務商不一樣而不一樣,但通常都會遵循http標準協議,經過http響應頭中的Cache-control: max-age的字段來設置CDN邊緣節點數據緩存時間。另外可經過CDN服務商提供的「刷新緩存」接口來更新緩存。
prebrowsing
預加載是瀏覽器對未來可能被使用資源的一種暗示,一些資源能夠在當前頁面使用到,一些可能在未來的某些頁面中被使用。做爲開發人員,咱們比瀏覽器更加了解咱們的應用,因此咱們能夠對咱們的核心資源使用該技術。
經過prebrowsing能夠提早緩存部分文件,可做爲一種靜態資源加載優化的手段。prebrowsing有如下幾種:
-
dns-prefetch:DNS預解析,告訴瀏覽器將來咱們可能從某個特定的 URL 獲取資源,當瀏覽器真正使用到該域中的某個資源時就能夠儘快地完成 DNS 解析。多在使用第三方資源時使用。
-
preconnect:預鏈接,完成 DNS 預解析同時還將進行 TCP 握手和創建傳輸層協議。
-
prerender:預渲染,預先加載文檔的全部資源,相似於在一個隱藏的 tab 頁中打開了某個連接 – 將下載全部資源、建立 DOM 結構、完成頁面佈局、應用 CSS 樣式和執行 JavaScript 腳本等。
-
prefetch:預獲取,使用 prefetch 聲明的資源是對瀏覽器的提示,暗示該資源可能『將來』會被用到,適用於對可能跳轉到的其餘路由頁面進行資源緩存。被 prefetch 的資源的加載時機由瀏覽器決定,通常來講優先級較低,會在瀏覽器『空閒』時進行下載。
-
preload:預加載,主動通知瀏覽器獲取本頁的關鍵資源,只是預加載,加載資源後並不會執行;
對於前面三種很多瀏覽器已經內部默認作了優化,而prefetch & preload須要開發者根據狀況代碼手動設置。
從
prefetch和
preload的瀏覽器支持狀況來看,prefetch除了safari外基本瀏覽器都有所支持,但preload做爲新出的規範,兼容性差些,但safari正慢慢支持這一標準,如在iOS的safari高級選項的
試驗性Webkit功能中已經有Link Preload這一選項。
preload 是聲明式的 fetch,能夠強制瀏覽器請求資源,同時不阻塞文檔
onload 事件,是對瀏覽器指示預先請求當前頁須要的資源(關鍵的腳本,字體,主要圖片)。
prefetch 提示瀏覽器這個資源未來可能須要,可是把決定是否和什麼時間加載這個資源的決定權交給瀏覽器。prefetch 應用場景稍微有些不一樣 —— 用戶未來可能在其餘部分(好比視圖或頁面)使用到的資源。
從以上的描述能夠看出,對於preload和prefetch聲明,preload明顯高於prefetch。
注:prebrowsing 好用但千萬不要亂用,除非你很是明確會加載要prebrowsing的文件,否則會加劇瀏覽器負擔拔苗助長。
接觸過
Next.js的同窗都知道,next.js提供了一個具備預獲取功能的模塊:next/prefetch,看起來功能與prefetch相似,但其優先級與preload相似。
<Link prefetch href='/'><a>Home</a></Link>
<Link prefetch href='/features'> <a>Features</a></Link>
{ /* we imperatively prefetch on hover */ }
<Link href='/about'>
<a onMouseEnter={() => { Router.prefetch('/about'); console.log('prefetching /about!') }}>About</a>
</Link>
<Link href='/contact'><a>Contact (<small>NO-PREFETCHING</small>)</a> </Link>複製代碼
因爲features連接設置了prefetch,訪問Index頁面時瀏覽器會在頁面加載完畢後從服務器取feature.js的文件,在index頁面訪問features頁面時不會再從服務器請求features.js文件,直接從本地緩存中讀取;contact沒有作處理,從index訪問contact時會從服務器請求concact.js文件。
咱們還能夠發現,在next.js打包出來的html文件頭中,都會將index.js / error.js / app.js 3個文件做爲preload加載,由於這3個文件是本頁面中必須用到的資源。
優化嘗試
雖然大多數html只會在每次發佈上線時纔會改變,如更新js/css資源的引用地址,因此通常將HTTP Headers中設置一個比較短的max-age值,如cache-control: max-age=300,除此以外建議服務器開啓Etag。
但以實時內容爲主的網站(如金融類)爲了頁面的打開速度,會採起後臺服務生產的方式 ,將全部首頁數據所有生成到html中,省去用戶首次加載時的後臺接口請求等待時間。通常會設置cache-control: no-cache。
如今通常都經過文件名進行版本控制。Webpack打包命名可根據文件內容生成文件名的hash值,每次打包只有當內容改才從新生成hash值。此種狀況之下,能夠在HTTP Headers設置一個較大的緩存時間,如max-age=2592000,儘可能避免304請求和服務器進行請求鏈接。
// js
output: {
path: config.build.assetsRoot,
filename: utils.assetsPath('js/[name].[chunkhash].js'),
chunkFilename: utils.assetsPath('js/[id].[chunkhash].js'),
}
// css
new ExtractTextPlugin({
filename: utils.assetsPath('css/[name].[contenthash].css'),
}),複製代碼
webfont文件比較特殊,正如
這篇文章中所說:
其實不一樣瀏覽器下載font文件的時間不太同樣,有的碰到css的聲明就會加載,有的會等到dom節點匹配css聲明時加載。
根據以上羅列的緩存建議,對當前的一個移動端項目進行優化。項目背景以下:
這個單頁頁面會打開幾個小的頁面(紅色圈部分),經過webpack打包以後大概這個樣子:
-
index.ef15ea073fbcadd2d690.js
-
static/js/0.1280b2229fe8e5582ec5.js
-
static/js/1.f3077ec7560cd38684db.js
-
static/js/2.39ecea8ad91ddda09dd0.js
-
static/js/3.d7ecc3abc72a136e8dc1.js
其中第一個index.js會在頁面初次加載,其餘4個js會在路由切換時動態加載。考慮下這個頁面的業務場景,只要進入到這個頁面,其餘幾個路由是必定會訪問到的。因此若是在頁面加載完成以後,趁戶思考之際就主動把剩下幾個js加載好,豈不完美。
webpackConfig.plugins.push(new PreloadWebpackPlugin({
rel: 'prefetch',
}));複製代碼
rel屬性還能夠選擇preload / prefetch模式。打包出來是這樣:
訪問頁面能夠看到,在不影響dom加載的狀況下,瀏覽器預先加載了另外幾個後面將會用到的js,當切換到對應路由時,也會直接從緩存取,不從服務器請求資源。
非動態加載(路由)頁面的css會單獨打包,在html文件中進行引用。除了使用一些打包插件優化代碼體積外,可將css更細粒度拆分,如首頁的css+彈窗css+頁面標籤切換的css等。除首頁css外的先預加載,而後動態獲取。但通常來講一個頁面的css大小在合理的代碼狀況下通過gzip壓縮後都不會過大,因此優化的效果並不會太明顯。
動態加載路由中css沒有單獨拆分而是在路由的js中,因此只能隨着js優化了。
對於font文件,除了減小文件大小,設置緩存時間以外,也能夠經過預加載的方式提早讓瀏覽器下載來提升首屏渲染速度。預加載webfont須要與webpack的
html-webpack-plugin結合,打包時將制定的字體插入到html中。網上找了一圈沒有找到現成的插件,本身來寫一個。
npm install fontpreload-webpack-plugin --save-dev複製代碼
- 在webpack的config文件的HtmlWebpackPlugin插件以後增長:
const FontPreloadWebpackPlugin = require('fontpreload-webpack-plugin');複製代碼
webpackConfig.plugins.push(new FontPreloadWebpackPlugin({
rel: 'prefetch',
fontNameList: ['fontawesome-webfont'],
crossorigin: true,
}));複製代碼