Web靜態資源緩存及優化

前言

對於頁面中靜態資源(html/js/css/img/webfont),理想中的效果:

  1. 頁面以最快的速度獲取到所有必須靜態資源,渲染飛快;
  2. 伺服器上靜態資源未更新時再次訪問不請求伺服器;
  3. 伺服器上靜態資源更新時請求伺服器最新資源,載入又飛快。

總結下來也就是2個指標:

  • 靜態資源載入速度
  • 頁面渲染速度

靜態資源載入速度引出了我們今天的主題,因為最直接的方式就是將靜態資源進行緩存。頁面渲染速度建立在資源載入速度之上,但不同資源類型的載入順序和時機也會對其產生影響,所以也留給了我們更多的優化空間。

當然除了速度,緩存還有另外2大功效,減少用戶請求的帶寬減少伺服器壓力

先用一張圖來概括下本文中將會涉及到的內容。

常見緩存類型

1、瀏覽器緩存

對於前端而言,這可能是我們最容易忽略的緩存類型,原因在於大部分設置都在伺服器運維層面上進行,不屬於前端開發的維護範圍。但靜態資源的內容更新時機其實前端是最清楚的,如果能在理解瀏覽器緩存策略的基礎上合理配置效果最佳。

瀏覽器緩存策略一般通過資源的Response Header來定義,html文件在很早之前的規範里也可以通過Meta標籤的http-equiv來定義。

一個Response Header示例:

可在w3c的官方文檔中查看所有HTTP Response Header欄位的定義,跟緩存相關的主要有上圖中被圈出來的幾個

  • Cache-Control:
    • 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和響應體。

Html Meta

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 -->n<meta http-equiv="Pragma" content="no-cache" /> <!-- 兼容HTTP1.0 -->n<meta http-equiv="Expires" content="0" /> <!-- 資源到期時間設為0 -->n

但現在w3c的規範欄位中這些值已經被移除,一個很好的理由是:

Putting caching instructions into meta tags is not a good idea, because although browsers may read them, proxies wont. 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更高一些。

2、HTML5 Application Cache

Application Cache是html5引入的本地存儲方案之一,可以構建離線緩存。目前除IE10-外其他瀏覽器均支持。

使用方法

a、增加manifest文件

application cache是通過mannifest文件來管理的,manifest文件是簡單的文本文件,內容是需要被緩存供離線使用的文件列表,及不需要被緩存或讀取緩存失敗的文件控制。

  • 文件的第一行必須是 CACHE MANIFEST
  • #開頭的行作為注釋語句
  • 網站的緩存不能超過5M
  • 文件資源路徑可以使用絕對路徑也可以使用相對路徑
  • 文件列表中任意一個緩存失敗都會導致整個緩存失效
  • 既可以站點使用同一個minifest文件,也可以每個頁面使用一個

文件包含3個指令

  • CACHE:需要緩存的資源文件,瀏覽器會自動緩存帶有manifest屬性的html頁面;
  • NETWORK:不需要緩存的文件,可以使用通配符;
  • FALLBACK:無法訪問緩存文件的備選文件,可以使用通配符。

b、伺服器配置

mannifest文件可以使用任意拓展名,但需要在伺服器中添加MIME類型匹配,使用apache比較簡單,如果使用.manifest作為拓展名在apache配置文件中添加。

AddType text/cache-manifest .appcachen

c、html中引用

<html lang="zh" manifest="main.manifest">n

註:千萬不要把manifest文件本身放在緩存文件列表中,不然瀏覽器無法更新manifest文件文件,最好在manifest文件的http headers中設置其立即過期。

緩存載入及更新過程

1、事件

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

2、執行過程

第一次載入

  • 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 代替。

3、PWA(Service Worker)

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》。

簡單使用

1、首先,要使用Service Worker,需要添加一個Service Worker的js的文件,然後在我們的html頁面中註冊對這個文件的引用。

index.html

<script>nnavigator.serviceWorkern .register(./sw.js)n .then(function (registration) {n // 註冊成功n });n</script>n

2、其次,我們在js文件中補充Service Worker的生命周期事件。Service Worker生命周期有三部曲:註冊,安裝和激活。

一般來說我們需要註冊的有3個事件:

self.addEventListener(install, function(event) { n /* 安裝後... */n // cache.addAll:把緩存文件加進來,如a.css,b.jsn});nnself.addEventListener(activate, function(event) {n /* 激活後... */n // caches.delete :更新緩存文件n});nnself.addEventListener(fetch, function(event) {n /* 請求資源後... */ n // cache.put 攔截請求直接返回緩存數據n});n

對於獲取文件和緩存文件,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才會被激活。

4、LocalStorage

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

5、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需要開發者根據情況代碼手動設置。

兼容性

從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>nn<Link prefetch href=/features> <a>Features</a></Link>nn{ /* we imperatively prefetch on hover */ }n<Link href=/about>n <a onMouseEnter={() => { Router.prefetch(/about); console.log(prefetching /about!) }}>About</a>n</Link>nn<Link href=/contact><a>Contact (<small>NO-PREFETCHING</small>)</a> </Link>n

由於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個文件是本頁面中必須用到的資源。

優化嘗試

不同文件類型

1、HTML文件

雖然大多數html只會在每次發布上線時才會改變,如更新js/css資源的引用地址,所以一般將HTTP Headers中設置一個比較短的max-age值,如cache-control: max-age=300,除此之外建議伺服器開啟Etag。

但以實時內容為主的網站(如金融類)為了頁面的打開速度,會採取後台服務生產的方式 ,將所有首頁數據全部生成到html中,省去用戶首次載入時的後台介面請求等待時間。一般會設置cache-control: no-cache。

2、js/css/img文件

現在一般都通過文件名進行版本控制。Webpack打包命名可根據文件內容生成文件名的hash值,每次打包只有當內容改才重新生成hash值。此種情況之下,可以在HTTP Headers設置一個較大的緩存時間,如max-age=2592000,盡量避免304請求和伺服器進行請求連接。

// jsnoutput: {n path: config.build.assetsRoot,n filename: utils.assetsPath(js/[name].[chunkhash].js),n chunkFilename: utils.assetsPath(js/[id].[chunkhash].js),n}n// cssnnew ExtractTextPlugin({n filename: utils.assetsPath(css/[name].[contenthash].css),n}),n

3、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文件

1、緩存配置

  • 對靜態資源文件進行如上的HTTP Headers緩存配置;
  • 所有的靜態資源文件通過Service Worker進行緩存控制和離線化載入,示範如上不再贅述;

2、其他優化

以其中一個單頁為例,頁面效果如下:

動態載入的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({n rel: prefetch,n}));n

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中。網上找了一圈沒有找到現成的插件,自己來寫一個。

1、寫插件

fontpreload-webpack-plugin

2、用插件

  • 安裝插件

npm install fontpreload-webpack-plugin --save-devn

  • 在webpack的config文件的HtmlWebpackPlugin插件之後增加:

const FontPreloadWebpackPlugin = require(fontpreload-webpack-plugin);nnwebpackConfig.plugins.push(new FontPreloadWebpackPlugin({n rel: prefetch,n fontNameList: [fontawesome-webfont],n crossorigin: true,n}));n

3、打包效果

本文內容到此結束,如有錯誤歡迎指正。


推薦閱讀:

Linux查看cache信息
HTTP 緩存機制一二三
緩存級數是否有上限?
在校學生一枚,面對高性能伺服器開發、分散式系統、緩存系統等等。該如何最快最好的提升自己的技術水平呢?
1G緩存的機械硬碟,拷貝文件是否只是前幾秒快一些?

TAG:前端开发 | 缓存 |