當我談緩存的時候,我談些什麼

TL;DR javascript

前面大段的內容都是基本概念的介紹,建議沒時間的同窗直接拖到最下面看。html

Web 緩存是能夠自動保存常見文檔副本的 HTTP 設備。對,當談到緩存的時候,就是指那些設備,如瀏覽器,代理緩存服務器等。java

經過網絡獲取內容既緩慢,成本又高:大的響應須要在客戶端和服務器之間進行屢次往返通訊,這拖延了瀏覽器可使用和處理內容的時間,同時也增長了訪問者的數據成本。所以,緩存和重用之前獲取的資源的能力成爲優化性能很關鍵的一個方面。node

使用緩存有下列的優勢:jquery

  • 緩存減小了冗餘的數據傳輸,節省了你的網絡費用。webpack

  • 緩存緩解了網絡瓶頸的問題,不須要更多的帶寬就可以更快的加載頁面。git

  • 緩存下降了對原始服務器的要求,服務器能夠更快的響應,避免過載的出現。github

  • 緩存下降了距離時延,由於從較遠的地方加載頁面會更慢一些。web

冗餘的數據傳輸

有不少小網站沒有對文檔作緩存處理,這樣客戶端每次訪問相同的文檔(例如 jQuery.js)的時候,都要從服務器下載相同的文檔到本地客戶端,形成大量的冗餘數據傳輸。gulp

帶寬瓶頸

緩存會緩解有限廣域網絡帶寬的瓶頸問題。不少網絡會爲本地客戶端提供的帶寬比爲遠程服務器提供的帶寬更寬。若是客戶端能夠從一個快速局域網的緩存中得到一份副本,天然能夠提升性能。

瞬間擁塞

12306 的春運,微博的春晚紅包等都會遇到這種狀況。12306 放票的時間段,會有大量的用戶去搶票,出現瞬間擁塞。瞬間擁塞可能會使網絡和 web 服務器發生崩潰。 DDOS 也是相同狀況。

距離時延

假設淘寶的主服務器都放在杭州的一臺服務器上。而在美國的客戶端打開了淘寶,須要下載淘寶的首頁;再假設數據的傳輸都是以光速的速度傳輸。杭州到華盛頓的距離大概有14,000千米,這樣光速自身傳輸就須要大概90ms的時間(算上請求和返回的時間),若是淘寶頁面上只有20個圖片,這樣單鏈接的狀況下,就大概須要(打開鏈接請求 90ms + GET web 頁面的90ms + GET 全部圖片的 90 * 20 = 1800 ms)1980ms 的時延。注意,這個只是時延。也就是說這個距離下 20 張圖片就會比客戶端在本地的請求延遲大概 2s 的時間。

命中和未命中的

  • 緩存命中(cache hit) 緩存的設備(能夠是代理緩存服務器,也能夠是本機)中有可使用的副本。

  • 緩存未命中(cache miss)緩存的設備中沒有可使用的副本,這個請求就會被轉發給原始服務器。

保持副本的新鮮

服務器上的文本內容隨時可能發生變化,如:淘寶首頁的一個文件中須要增長記錄用戶點擊日誌的功能,因此須要修改某個js文件,以增長對應的功能。對於這種狀況,緩存就要不時的對其進行檢測,看看它們保存的副本是否還是服務器上最新的副本。對於這種檢測,就被稱爲新鮮度檢測,這些新鮮度檢測就被稱爲 HTTP 再驗證

再驗證

爲了有效的進行再驗證,HTTP 定義了一些特殊的請求,不用從服務器上獲取整個對象,就能夠快速檢測出內容是不是最新的。最經常使用的是 If-Modified-Since 首部(後面的內容會提一下 ETag 和 If-None-Match)。當這個首部被加入到 GET 請求中去,就能夠告訴服務器:只有緩存了對象的副本以後,又對其進行了修改的狀況下,才發送此對象。

對於服務器接收到 GET If-Modified-Since 請求時大概會發生如下三種狀況:

  • 再驗證命中

若是服務器對象未被修改,服務器回想客戶端發送一個小的 HTTP 304 Not Modified 響應。

  • 再驗證未命中

若是服務器對象與已緩存副本不一樣,服務器向客戶端發送一條普通的、帶有完整內容的 HTTP 200 OK 的響應。

  • 對象被刪除

若是服務器對象已經被刪除了,服務器就會回送一個 404 Not Found 響應,緩存也會將其副本刪除。

If-Modified-Since 是 HTTP 請求首部,能夠與 Last-Modified 服務器響應首部配合工做。原始服務器會將最後的修改日期附加到所提供的文檔上去。當緩存要對已緩存文檔進行再驗證時,就會包含一個 If-Modified-Since 首部,其中攜帶有最後修改已緩存副本的日期。

<!-- test.html 最後一次修改時間: 2016-3-12 20:03 -->

<!Doctype html>
<html>
<head>
  <title>hello</title>
</head>
<body>
  <div>no cache</div>
</body>
</html>
// demo1.js 

'use strict'
const http = require('http')
const fs = require('fs')

const onRequest = (req, res) => {
  const filepath = './test.html'
  , file = fs.readFileSync(filepath)
  , stats = fs.statSync(filepath)
  , mtime = stats.mtime
  , reqMtimeString = req.headers["if-modified-since"]

  let status = 200

  if(reqMtimeString) {
    const reqMtime = new Date(reqMtimeString)
    if(reqMtime.getTime() === mtime.getTime()) status  = 304
  }

  res.writeHead(status, {'Content-Type': 'text/html', 'Last-Modified': mtime})
  if(200 === status) res.write(file)
  res.end()
}

http.createServer(onRequest).listen('8000', () => console.log('server start:8000'))

上面是用 Node.js 寫了一個簡易的服務器,檢測 test.html 是否有變化,若是最後一次修改的時間和客戶端的時間不一樣的話,就返回新鮮的文檔。

經過 node demo.js 運行服務器。打開瀏覽器的開發者工具(記得把 disable cache 的選項勾掉),能夠看到此時HTTP請求的 header 爲:

General

Request URL:http://localhost:8000/
Request Method:GET
Status Code:200 OK
Remote Address:[::1]:8000


Response Headers

HTTP/1.1 200 OK
Content-Type: text/html
...
Last-Modified: Sat Mar 12 2016 20:03:58 GMT+0800 (CST)


Request Headers

GET / HTTP/1.1
Host: localhost:8000
...
If-Modified-Since: Sat Mar 12 2016 20:03:58 GMT+0800 (CST)

此時的返回的狀態碼爲 200, 服務器設置了 Last-Modified 首部以後,瀏覽器端會加上 If-Modified-Since 的頭部。以後再刷新瀏覽器,查看開發者工具,發現通常頭(即 General)的 status code 變成 304 Not Modified,即上述的再驗證命中

再修改 test.html 的內容:

<!-- test.html 最後一次修改時間: 2016-3-12 20:26 -->

<!Doctype html>
<html>
<head>
  <title>hello</title>
</head>
<body>
  <div>change cache</div>
</body>
</html>

刷新瀏覽器,此時的 header 以下:

General

Request URL:http://localhost:8000/
Request Method:GET
Status Code:200 OK
Remote Address:[::1]:8000


Response Headers

HTTP/1.1 200 OK
Content-Type: text/html
...
Last-Modified: Sat Mar 12 2016 20:26:36 GMT+0800 (CST)


Request Headers

GET / HTTP/1.1
Host: localhost:8000
...
If-Modified-Since: Sat Mar 12 2016 20:03:58 GMT+0800 (CST)

能夠看到通常頭的 status code 又變成了 200,且響應頭的 Last-Modified 變成最後一次修改時間,即上述的再驗證未命中,服務器會返回修改後的文件。

對象被刪除的狀況就再也不寫代碼驗證了。

文檔過時

服務器也能夠經過添加一個 HTTP Cache-Control 首部和 Expires 首部讓緩存能夠在緩存文檔未過時的狀況下隨意使用這些文檔副本。

HTTP/1.0 的 Expires 首部或 HTTP/1.1 的 Cache-Control: max-age 響應首部來指定過時日期。Expires 使用的是絕對日期,絕對日期依賴於計算機時鐘的正確設置,若是計算機時鐘不正確,會形成緩存的過時日期不正確,可能就達不到緩存的初衷,因此在 HTTP/1.1 就增長了 Cache-Control: max-age 來替代 Expires 。

max-age 響應首部表示的是從服務器將文檔傳來之時起,能夠認爲此文檔處於新鮮狀態的秒數,還有一個s-maxage的首部,其行爲與 max-age 相似,僅適用於共享緩存。

服務器能夠請求緩存不要緩存文檔(Cache-Control: no-store),或者將最大使用期設置爲零(Cache-Control: max-age=0),從而在每次訪問的時候都進行刷新。

下面是一段 Nodejs 實現的 max-age 代碼:

// demo2.js
'use strict'
const http = require('http')
const fs = require('fs')

const onRequest = (req, res) => {
  if('/req.js' === req.url) {
    let filepath = './req.js'
    , file = fs.readFileSync(filepath)

    res.writeHead(200, {'Content-Type': 'text/javascript', 'Cache-Control': 'max-age=60'})
    res.write(file)
    res.end()
  } else {
    let filepath = './test2.html'
    , file = fs.readFileSync(filepath)

    res.writeHead(200, {'Content-Type': 'text/html'})
    res.write(file)
    res.end()
  }
}

http.createServer(onRequest).listen('8000', () => console.log('server start:8000'))
// req.js
'use strict'
console.log(123)
<!-- test2.html -->
<!Doctype html>
<html>
<head>
  <title>hello</title>
</head>
<body>
  <div>no cache</div>
<script src='/req.js'></script>
</body>

</html>

打開瀏覽器,先打開開發者工具,再輸入地址以後,按回車能夠看到下圖,req.js 沒有被緩存。

no-cahce

從新再瀏覽器輸入地址回車(手動刷新和 cmd+r 屬於強制刷新,會清除緩存),能夠看到下圖 req.js 已經被緩存了(from cache):

cache

因爲在服務器上設置的緩存失效時間是 60s,因此 60s 以後再看,此時的緩存已經失效,又會像第一幅圖同樣, req.js 沒有 from cache。

Etag 和 If-None-Match

HTTP 容許用戶對 Etag 的版本標識符進行比較。在服務器端設置 Etag 首部以後,客戶端會對應的生成 If-None-Match 首部。服務器端能夠經過 If-None-Match 首部和對應的文檔內容的hash值或者其它指紋信息進行校驗,來決定是否返回新鮮的文檔。

最優的緩存策略

因爲使用 Etag,服務器端每次都要對文檔內容 hash 來肯定是否返回新鮮的文檔,仍是會浪費大量的服務器資源,因此 Etag 的緩存策略不建議使用。

因此結合 Google 給出的最優緩存策略,總結以下:

  • HTML 被標記成no-cache,這意味着瀏覽器在每次請求時都會從新驗證文檔,若是內容更改,會獲取最新版本。同時,在 HTML 標記中,咱們在 CSS 和 JavaScript 資源的網址中嵌入指紋碼:若是這些文件的內容更改,網頁的 HTML 也會隨之更改,並將下載 HTML 響應的新副本。

  • 容許瀏覽器和中繼緩存(例如 CDN)緩存 CSS,過時時間設置爲 1 年。注意,咱們能夠放心地使用 1 年的’遠期過時’,由於咱們在文件名中嵌入了文件指紋碼:若是 CSS 更新,網址也會隨之更改。

  • JavaScript 過時時間也設置爲 1 年,可是被標記爲 private,也許是由於包含了 CDN 不該緩存的一些用戶私人數據。

  • 緩存圖片過時時間儘可能設置超長。

上面第一條所說的指紋碼通常是指文檔內容的 hash 值,這個能夠經過 gulp,webpack 等打包工具在生成文件的時候就生出 hash 值,附在文件名後面,例如:jquery.min.js,根據文檔生成的 hash 值爲 1iuiqe981823,文件名能夠自動生成爲: jquery.min.1iuiqe981823.js。這樣既能夠保證在文檔沒有變化是能夠從緩存中讀取,又能夠保證文檔在有變化能夠及時更新。

最後

本文大部份內容都是直接引用『HTTP 權威指南』,最後一部分的最優策略是參考 Google Developers 的文檔。有些許內容是理解以後給出的代碼實現或驗證。

原文

相關文章
相關標籤/搜索