HTTP/1.x 及 Service Worker 緩存實踐小結

在錯縱複雜的網絡環境下,如何將頁面快速得傳遞給用戶是前端們的職責,而在此以後,如何減小網絡傳輸的花費一樣值得咱們關注。本文以 HTTP/1.x 和 Service Worker 緩存兩個方面,就如何減小網絡傳輸成本爲目標,探討下筆者最近對於緩存的實踐,權當拋磚引玉 🤪css

HTTP 緩存

The performance of web sites and applications can be significantly improved by reusing previously fetched resources. Web caches reduce latency and network traffic and thus lessen the time needed to display a representation of a resource. By making use of HTTP caching, Web sites become more responsive.html

根據 MDN 定義可知道,緩存是對已獲取資源的從新利用,是提高 WEB 性能的重要指標。根據是否和 Server 進行交互,HTTP 緩存分爲兩類:前端

  • 強制緩存
  • 協商緩存

強制緩存是無需和 Server 進行交互,直接在 Client 進行緩存。 而協商緩存須要和 Server 交互來判斷是否重用緩存。react

HTTP 緩存首部有如下幾種:webpack

  • Expires
  • Cache-Control
  • ETag/If-None-Match
  • Last-Modified/If-Modified-Since

Expires

語法:Expires: <http-date>git

Expires 經過設置一個時間戳,控制緩存的過時時間點。但缺點是客戶端時間和服務器時間可能不一致,沒法保證緩存的同步性。github

此外,若是存在 Cache-Control 首部並設置了max-age指令,Expires 首部將被忽略。web

Cache-Control

語法:`Cache-Control: [public | private | no-cache | only-if-cached],max-age=|s-maxage=|max-stale[=]|min-fresh=][,must-revalidate|proxy-revalidate|immutable][,no-store|no-transform]算法

具體配置細節見 MDN,屬於強制緩存,再也不贅述。這裏只講下本身實踐所用到的設置項。json

  • public | private
  • max-age=<seconds>
  • no-cache | no-store | must-revalidate

publicprivate 定義了緩存的共享性,分爲共享(public)與私有(private)緩存。共享緩存存儲的響應可以被多個用戶使用,私有緩存只能用於單獨用戶。 共享緩存可存在於 ISP、網關或 CDN 的節點上,能很大程度緩存熱門資源,減小網絡擁堵與延遲,但存在中間人攻擊的風險,故存在private緩存 —— 只緩存在用戶的瀏覽器端,不會被共享。可根據本身的業務需求,選擇是私有仍是共享的。

max-age=<seconds> 規定了緩存時長,以秒爲單位。從開始接收到資源爲時間點,在接下來的 max-age 時間內使用緩存。理論上來講能夠長期緩存,但帶來的問題是瀏覽器緩存的臃腫,根據 RFC2616 最長時常設爲一年較爲合適,即 Cache-Control: max-age=31536000

no-cacheno-storemust-revalidateno-cache 規定使用緩存以前時必定要通過驗證,好比驗證 ETag/ Last-Modified 等; no-store 直接禁止瀏覽器以及全部中間緩存存儲任何版本的返回響應,每次用戶請求該資產時,都會向服務器發送請求,並下載完整的響應;must-revalidate 緩存必須在使用以前驗證舊資源的狀態,而且不可以使用過時資源。

ETag/If-None-Match

ETag 是對資源的一個特殊標誌符,能惟一肯定資源。語法:

ETag: [W/]"<etag_value>"
複製代碼

W/代表了資源是否採用弱類型驗證器進行比較,其較爲容易生成但不利於比較。"<etag_value>" 是對資源的惟一標誌符,其值是一串 ASCII 字符串。生成規則沒有必定的要求,但常採用的生成算法是內容的 hash 值加上內容的最後修改時間。

當響應頭部包含 ETag 時,下次請求時瀏覽器會自動帶上 If-None-Match: <last_etag_value> 首部,用來驗證資源是否過時。 若是已過時,則以 HTTP 200 返回新的內容響應並帶上新的 ETag。若是資源未過時,則返回 HTTP 304 告知瀏覽器資源未過時能夠繼續使用。

Last-Modified/If-Modified-Since

語法:Last-Modified: <day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT

顧名思義,Last-Modified/If-Modified-Since 是根據內容最後的修改時間來判斷是否採用緩存的方法。但因爲最小時間單位爲秒,對於要求時間比較精細的資源可能不太適用。

緩存優先級

HTTP/1.x 緩存首部的優先級: Cache-Control > Expires > ETag/If-None-Match > Last-Modified/If-Modified-Since, 即在同時設定了上述首部時Cache-Control 最高,可根據業務需求設定。

以上,即是 HTTP/1.x 緩存設置的首部解釋,能夠經過Browser Caching Checker 對瀏覽器緩存進行檢查。

Cache Checker

Server Worker 緩存

當下時間點,Service Worker 在瀏覽器上的支持度已高達 86.16%, 因此是時候考慮開啓 Service Worker 來加速你的網站了。不只能夠利用 Service Worker 所帶來的緩存好處,還能很容易遷移到 PWA,更大程度發掘 Web App 的能力。

不一樣於 HTTP 緩存,Server Worker 不只能動態緩存資源,並且還能提供 offline 模式,對弱網絡環境的用戶極爲友好。開啓 Service Worker 大概須要註冊、安裝、緩存資源、更新和註銷等過程。

Server Worker 生命週期

接下來以一個小 Demo 爲例,簡單介紹如何開啓一個 Service Worker 服務。源碼見 sw-cache-example

註冊

註冊流程很簡單,只須要判斷瀏覽器是否支持 Service Worker 特性,並在頁面 Load 以後,註冊 Service Worker 服務,關鍵代碼:

// sw-reg.js

if ('serviceWorker' in navigator) {
  window.addEventListener('load', function() {
    navigator.serviceWorker.register('./sw.js').then(
      function(registration) {
        // Registration was successful
        console.log('ServiceWorker registration successful with scope: ', registration.scope)
      },
      function(err) {
        // registration failed :(
        console.log('ServiceWorker registration failed: ', err)
      }
    )
  })
}
複製代碼

安裝

安裝過程須要作的有:監聽 install 事件,並在其回調事件內緩存資源。

var CACHE_NAME = 'cache-v1'
var urlsToCache = ['/', '/styles/main.css', '/script/main.js']

self.addEventListener('install', function(event) {
  // Perform install steps
  event.waitUntil(
    caches.open(CACHE_NAME).then(function(cache) {
      console.log('Opened cache')
      return cache.addAll(urlsToCache)
    })
  )
})
複製代碼

響應緩存

最重要的一步,就是在資源被緩存後利用緩存了。須要作的也很簡單:監聽 fetch 事件 -> 對已緩存的資源進行響應。

self.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.match(event.request).then(function(response) {
      if (response) {
        return response
      }
      return fetch(event.request)
    })
  )
})
複製代碼

更新

更新也是 Service Worker 很重要的一步,其過程也很易懂:驗證資源是否過時 -> 對過時的資源進行刪除並緩存新的資源。

self.addEventListener('activate', function(event) {
  var cacheWhitelist = ['pages-cache-v1', 'blog-posts-cache-v1']

  event.waitUntil(
    caches.keys().then(function(cacheNames) {
      return Promise.all(
        cacheNames.map(function(cacheName) {
          if (cacheWhitelist.indexOf(cacheName) === -1) {
            return caches.delete(cacheName)
          }
        })
      )
    })
  )
})
複製代碼

註銷

註銷只須要拿到 Service Worker 實例,調用 unregister 便可。

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.ready.then(registration => {
    registration.unregister()
  })
}
複製代碼

至此,基本完成了 Service Worker 的基本部署,開啓其提供的緩存能力。

😷 實踐過程當中遇到的坑

  1. 遷移 HTTP 請求方法爲fetch

因爲在響應緩存時,須要經過監聽 fetch 事件來響應緩存,故須要更改 HTTP 請求方法爲 fetch,其 API 參見 MDN。 對於不支持 fetch 的瀏覽器,可使用這個 fetch 進行打補丁。

  1. 取消 fetch 請求

因爲 fetch 沒有提供原生的取消方法,故須要使用 signal 來取消 fetch 請求。

const controller = new AbortController()
const signal = controller.signal

fetch('/some/url', { signal })
  .then(res => res.json())
  .then(data => {
    // do something with "data"
  })
  .catch(err => {
    if (err.name == 'AbortError') {
      return
    }
  })

// controller.abort(); // can be called at any time
複製代碼

Polyfill 參照 abortcontroller-polyfill

  1. 增長 Service Worker 開關 Service Worker 提供的緩存雖然好用,但有時候須要根據業務註銷 Service Worker, 這時就須要一個開關來控制。並且應該在第一次部署的時候就增長開關,對於緩存進行控制。
fetch(API.switch)
  .then(res => {
    const isOn = res.status
    if (isOn) {
      sw.register()
    } else {
      sw.unregister()
    }
  })
  .catch(err => {
    console.error('fetch sw status error', err)
  })
複製代碼
  1. 對入口文件取消緩存 對於通常的 SPA,是經過入口文件進行資源的索引,因此對入口文件應該不予緩存,並要求其強制更新。在使用sw-precache-webpack-plugin應排除入口文件:
new SWPrecacheWebpackPlugin(
    {
      cacheId: 'my-project-name',
      dontCacheBustUrlsMatching: /\.\w{8}\./,
      filename: 'service-worker.js',
      minify: true,
      navigateFallback: PUBLIC_PATH + 'index.html',
      staticFileGlobsIgnorePatterns: [/\.map$/, /asset-manifest\.json$/, /index\.html$/],
    }
  ),
複製代碼

對入口文件能夠設置 HTTP 響應首部:

Cache-Control: no-cache, no-store, must-revalidate
複製代碼

其含義是不使用本地及任何中間存儲緩存,必須和服務器取得驗證才能拿到新的內容。

  1. 若是不想本身編寫 Service Worker, 能夠參照網上的模板或插件 🤣:

總結

  1. 使用 Cache-Control 對靜態資源進行長期緩存,配合 webpack 打包生成的文件 hash 名,可所有采用這一策略
  2. 使用 ETag/If-None-Match 對內容 hash 進行精確緩存
  3. 對於時間要求不精確的資源,使用 Last-Modified/If-Modified-Since 對修改時間對內容進行緩存,以替代使用ETag/If-None-Match對 CPU 的高消耗
  4. 使用Service Worker 提供動態緩存和離線能力

因此,如今開始打開調試工具,爲你的網站增長緩存吧~ ✌️

Reference

  1. HTTP Caching
  2. Browser Caching Checker
  3. Understanding The Vary Header
  4. ServiceWorker Cache
相關文章
相關標籤/搜索