Web靜態資源緩存及優化

前言

對於頁面中靜態資源(html/js/css/img/webfont),理想中的效果:
  1. 頁面以最快的速度獲取到全部必須靜態資源,渲染飛快;
  2. 服務器上靜態資源未更新時再次訪問不請求服務器;
  3. 服務器上靜態資源更新時請求服務器最新資源,加載又飛快。
總結下來也就是2個指標:
  • 靜態資源加載速度
  • 頁面渲染速度
靜態資源加載速度引出了咱們今天的主題,由於最直接的方式就是將靜態資源進行緩存。頁面渲染速度創建在資源加載速度之上,但不一樣資源類型的加載順序和時機也會對其產生影響,因此也留給了咱們更多的優化空間。
固然除了速度,緩存還有另外2大功效,減小用戶請求的帶寬和減小服務器壓力。

先用一張圖來歸納下本文中將會涉及到的內容。css


常見緩存類型

一、瀏覽器緩存html

對於前端而言,這多是咱們最容易忽略的緩存類型,緣由在於大部分設置都在服務器運維層面上進行,不屬於前端開發的維護範圍。但靜態資源的內容更新時機其實前端是最清楚的,若是能在理解瀏覽器緩存策略的基礎上合理配置效果最佳。
瀏覽器緩存策略通常經過資源的Response Header來定義,html文件在很早以前的規範裏也能夠經過Meta標籤的http-equiv來定義。

一個Response Header示例:

可在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中會攜帶如下兩個字段:
  • If-Modified-Since:值爲以前response中Last-Modified;
  • If-None-Match:值爲以前response中Etag(若是存在的話);
其中在圖右側的「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-外其餘瀏覽器均支持。
使用方法

a、增長manifest文件react

application cache是經過mannifest文件來管理的,manifest文件是簡單的文本文件,內容是須要被緩存供離線使用的文件列表,及不須要被緩存或讀取緩存失敗的文件控制。
  • 文件的第一行必須是 CACHE MANIFEST
  • #開頭的行做爲註釋語句
  • 網站的緩存不能超過5M
  • 文件資源路徑可使用絕對路徑也可使用相對路徑
  • 文件列表中任意一個緩存失敗都會致使整個緩存失效
  • 既能夠站點使用同一個minifest文件,也能夠每一個頁面使用一個
文件包含3個指令
  • CACHE:須要緩存的資源文件,瀏覽器會自動緩存帶有manifest屬性的html頁面;
  • NETWORK:不須要緩存的文件,可使用通配符;
  • FALLBACK:沒法訪問緩存文件的備選文件,可使用通配符。

b、服務器配置webpack

mannifest文件可使用任意拓展名,但須要在服務器中添加MIME類型匹配,使用apache比較簡單,若是使用.manifest做爲拓展名在apache配置文件中添加。
AddType text/cache-manifest .appcache複製代碼

c、html中引用git

<html lang="zh" manifest="main.manifest">複製代碼
注:千萬不要把manifest文件自己放在緩存文件列表中,否則瀏覽器沒法更新manifest文件文件,最好在manifest文件的http headers中設置其當即過時。

緩存加載及更新過程


一、事件github

  • cached/checking/downloading/error/noupdate/obsolete/progress/updateready

二、執行過程web

第一次加載:
  • 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 Downloading event(開始下載緩存文件)
      • Application Cache Progress event (0 of 4)(依次下載緩存文件)
      • ……
      • Application Cache Progress event (4 of 4)
      • Application Cache UpdateReady event(緩存文件更新完畢)
      • Application Cache NoUpdate event(啥也不作)
刪除html中manifest文件引用
  • Document was loaded from Application Cache with manifest(從緩存中讀取html文件和其餘靜態資源文件,供頁面展現)
  • Application Cache Checking event(獲取新的manifest文件,檢查是否更新)
  • Application Cache Obsolete event(刪除本地緩存中的全部文件,再也不使用緩存)

一些問題
  1. Application Cache會默認緩存引用manifest文件的HTML文檔,對於動態更新的html頁面來講是個坑(可使用tricky的iframe嵌入方式來避免);
  2. 只要緩存列表中的一個資源加載失敗,全部文件都將緩存失敗;
  3. 若是資源沒有被緩存,而又沒有設置NETWORK的狀況下,將會沒法加載,因此Network中必須使用通配符配置;
  4. 緩存更新後第一次只能加載manifest文件,其餘靜態資源須要第二次加載才能看到最新效果;
  5. 緩存文件清單中的文件自己更新瀏覽器是不會從新緩存,那怎麼告訴瀏覽器緩存須要更新了呢?
    • 更新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的離線使用、消息推送、後臺自動更新等能力。
不過它有如下限制:
  • 不能訪問 DOM
  • 不能使用同步 API
  • 須要HTTPS協議(http://localhost 或 http://127.0.0.1也可)
雖然如今其 瀏覽器支持狀況並非很普遍,但之後應該會大面積支持。本文作簡單介紹,具體使用方法能夠參考官方文檔《 The Offline Cookbook》。

簡單使用

一、首先,要使用Service Worker,須要添加一個Service Worker的js的文件,而後在咱們的html頁面中註冊對這個文件的引用。
index.html
<script>
navigator.serviceWorker
    .register('./sw.js')
   .then(function (registration) {
       // 註冊成功
   });
</script>複製代碼

二、其次,咱們在js文件中補充Service Worker的生命週期事件。Service Worker生命週期有三部曲:註冊,安裝和激活。
通常來講咱們須要註冊的有3個事件:
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(應用數據的內容存儲,此緩存獨立於瀏覽器緩存和網絡狀態)。
React腳手架 create-react-app中已經內置了PWA功能,咱們來看下打包後的build文件夾下的文件結構:

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

LocalStorage雖是瀏覽器端緩存一種,但有多少人會用它來緩存文件呢?首先緩存讀取須要依靠js的執行,因此前提條件就是可以讀取到html及js代碼段;其次文件的版本更新控制會帶來更多的代碼層面的維護成本,因此LocalStorage更適合關鍵的業務數據而非靜態資源。


五、CDN緩存

這是一種以空間換時間的方案,減小了用戶的訪問延時,也減小的源站的負載。
客戶端瀏覽器先檢查是否有本地緩存是否過時,若是過時,則向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須要開發者根據狀況代碼手動設置。

兼容性

prefetchpreload的瀏覽器支持狀況來看,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文件

雖然大多數html只會在每次發佈上線時纔會改變,如更新js/css資源的引用地址,因此通常將HTTP Headers中設置一個比較短的max-age值,如cache-control: max-age=300,除此以外建議服務器開啓Etag。
但以實時內容爲主的網站(如金融類)爲了頁面的打開速度,會採起後臺服務生產的方式 ,將全部首頁數據所有生成到html中,省去用戶首次加載時的後臺接口請求等待時間。通常會設置cache-control: no-cache。

二、js/css/img文件

如今通常都經過文件名進行版本控制。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

webfont文件比較特殊,正如 這篇文章中所說:
  • 瀏覽器在DOMNode的CSS選擇器中發現@font-face時纔會下載web fonts文件,這個時候瀏覽器已經下載完成html/css/js文件;
  • 若是在瀏覽器發現須要加載font文件以前就告訴瀏覽器下載font文件,會加快文件下載和頁面加載速度。
其實不一樣瀏覽器下載font文件的時間不太同樣,有的碰到css的聲明就會加載,有的會等到dom節點匹配css聲明時加載。

優化實踐
根據以上羅列的緩存建議,對當前的一個移動端項目進行優化。項目背景以下:
  • React + + Mobx + Webpack
  • React-Router 單頁 / bundle-loader動態加載 / 使用較大的webfont文件
一、緩存配置
  • 對靜態資源文件進行如上的HTTP Headers緩存配置;
  • 全部的靜態資源文件經過Service Worker進行緩存控制和離線化加載,示範如上再也不贅述;
二、其餘優化
以其中一個單頁爲例,頁面效果以下:

動態加載的js

這個單頁頁面會打開幾個小的頁面(紅色圈部分),經過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加載好,豈不完美。
在此選用了 preload-webpack-plugin這個插件,它能夠打包將動態路由進行預加載。
webpackConfig.plugins.push(new PreloadWebpackPlugin({
    rel: 'prefetch',
}));複製代碼

rel屬性還能夠選擇preload / prefetch模式。打包出來是這樣:

訪問頁面能夠看到,在不影響dom加載的狀況下,瀏覽器預先加載了另外幾個後面將會用到的js,當切換到對應路由時,也會直接從緩存取,不從服務器請求資源。


css文件

非動態加載(路由)頁面的css會單獨打包,在html文件中進行引用。除了使用一些打包插件優化代碼體積外,可將css更細粒度拆分,如首頁的css+彈窗css+頁面標籤切換的css等。除首頁css外的先預加載,而後動態獲取。但通常來講一個頁面的css大小在合理的代碼狀況下通過gzip壓縮後都不會過大,因此優化的效果並不會太明顯。

動態加載路由中css沒有單獨拆分而是在路由的js中,因此只能隨着js優化了。


webfont文件

對於font文件,除了減小文件大小,設置緩存時間以外,也能夠經過預加載的方式提早讓瀏覽器下載來提升首屏渲染速度。預加載webfont須要與webpack的 html-webpack-plugin結合,打包時將制定的字體插入到html中。網上找了一圈沒有找到現成的插件,本身來寫一個。

一、寫插件 

fontpreload-webpack-plugin

二、用插件

  • 安裝插件
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,
}));複製代碼

三、打包效果



本文內容到此結束,若有錯誤歡迎指正。
相關文章
相關標籤/搜索