淘寶網站上的 HTTP 緩存問題兩則

在閱讀本文前推薦你先閱讀個人前兩篇文章《 扼殺 304,Cache-Control: immutable》和《關於緩存和 Chrome 的「新版刷新」》;下面要說的兩個問題是在淘寶(包括天貓等等)任意主流頁面中都存在的,因此你能夠隨便打開一個頁面進行測試;這兩個問題我去年在微博上都簡單提到過,這裏作一下梳理總結。javascript

一. 部分圖片文件始終 304,沒法直接讀取緩存

淘寶網站上什麼類型的請求最多?固然是圖片了。拿淘寶首頁舉例,在 Chrome 的新標籤頁中先打開開發者工具,再打開淘寶首頁,而後滾動到頁面最底部,在開發者工具的網絡面板中點擊 Img 篩選條件後可以看到左下角有相似以下的數字:html

有高達 80% 的請求數都是圖片,淘寶的其它主流頁面也有一樣的規律。然而在這些圖片中,有一部分圖片沒法直接讀取瀏覽器緩存,即使已經被下載過,瀏覽器也要再發個條件請求,在收到 CDN 返回的空的 304 響應後再讀取緩存:java

<div><a href="javascript:location+=''">點擊該連接,從而使當前頁面從新加載,下面的兩張圖片應該直接讀取緩存,不發起任何 HTTP 請求</a></div>
<img width=100 src="http://images2015.cnblogs.com/blog/116671/201702/116671-20170222201116820-1249825884.png">
<img width=100 src="https://img.alicdn.com/imgextra/i2/272205633/TB2J1fpaurAQeBjSZFwXXa_RpXa_!!272205633.jpg">

上面這個 demo 中有兩張圖, 一張是存在淘寶 CDN 上的,另外一張是我轉存到博客園的。當你點擊測試連接時會發現,博客園上的圖片可以直接讀取緩存(沒發送請求),而淘寶 CDN 上的那張圖產生了個 304 請求(響應碼爲 304 的請求):jquery

這個視頻演示有下面三個要關注的點:chrome

1. 那個 304 請求的響應有 49 個字節。瀏覽器

2. 那個 304 請求的響應時間爲十幾毫秒到幾百毫秒不等。緩存

3. 頁面中直接讀取緩存的圖片絲絕不動,而通過 304 後再讀取緩存的圖片有明顯的閃動。服務器

別看請求頭加上響應頭也就 100 個字節,但架不住多啊,這種圖片的日 pv 我估計至少得用百億作單位,浪費的日流量得用 T 做單位,這種小錢也許對淘寶這種大廠來講徹底能夠忽略,但用戶體驗倒是無法忽略的,在理想的網絡狀況下圖片展示都有明顯的閃動,那在移動端或者網絡環境較差的 PC 端,延時就會更加明顯了。網絡

緣由是什麼?讓咱們看看這張圖片的響應頭(已經刪掉了 x- 開頭的):app

$ curl -I 'https://img.alicdn.com/imgextra/i2/272205633/TB2J1fpaurAQeBjSZFwXXa_RpXa_!!272205633.jpg'

HTTP/1.1 200 OK

Server: Tengine

Content-Type: image/jpeg

Content-Length: 94506

Connection: keep-alive

Date: Mon, 31 Oct 2016 02:43:49 GMT

last-modified: Fri, 09 Sep 2016 08:42:30 GMT

Cache-Control: max-age=3600, s-maxage=31536000

Access-Control-Allow-Origin: *

Via: cache64.l2et2[0,200-0,H], cache23.l2et2[21,0], cache4.cn395[0,200-0,H], cache3.cn395[0,0]

Age: 9938377

Timing-Allow-Origin: *

EagleId: 8ccd3b4314878202062633285e

問題就出在標紅的這兩個頭上,瀏覽器看一個緩存有沒有過時是經過看 Date 頭返回的時間點加上 Cache-Control 頭中 max-age 字段指定的時間段算出的時間點有沒有小於客戶端的時間,也就是說看那個算出的時間是否是尚未到來。對於這張圖片的話,過時時間能夠經過以下的 JS 代碼計算出來:

new Date(+new Date("Mon, 31 Oct 2016 02:43:49 GMT") + 3600) + "" // "Mon Oct 31 2016 10:43:52 GMT+0800 (CST)"

其實這個例子根本不須要筆算,口算都能算出來,2016 年 10 月份的某個時刻加上一個小時確定仍是 2016 年,小於個人客戶端時間 2017 年,因此瀏覽器剛剛獲取到這張圖片就已通過期了。Firefox 有個內部調試工具能夠看到每一個緩存的過時時間,在 Firefox 中打開那張圖片後再打開 about:cache-entry?storage=disk&context=&eid=&uri=https://img.alicdn.com/imgextra/i2/272205633/TB2J1fpaurAQeBjSZFwXXa_RpXa_!!272205633.jpg 頁面:

expires 字段就是 Firefox 計算出來的過時時間,沒有顯示 2016 年是由於若是 Firefox 計算出的過時時間是過去的某個時間,會用當前時間來代替。

因此問題就是爲何 CDN 返回的 Date 會是 2016 年?是由於這張圖片是在 2016 年回源的,回源的時候 CDN 緩存了當時圖片源站返回的 Date 頭,做爲之後給瀏覽器返回的 Date 頭,因此用戶瀏覽器接受到的 Date 就固定在了 2016 年。

既然是 CDN 的問題,爲何 CDN 上的其它圖片和文件沒有一樣的表現?是由於 Cache-Control。這類圖片的 Cache-Control 有個特色,那就是 max-age 比 s-maxage 小,咱們知道 max-age 是 CDN 給瀏覽器用的,而 s-maxage 是源站給 CDN 用的,max-age=3600, s-maxage=31536000 表明的含義就是瀏覽器只能緩存這張圖片一小時,而 CDN 會緩存這張圖片一年,因此只有等到了 Mon, 31 Oct 2017 02:43:49 GMT 年的時候,這張圖片的 Date 響應頭纔會更新,也就是對於用戶來講,一年中有 364 天 23 小時 訪問這張圖片都是直接過時的。

所以只要 max-age 不比 s-maxage 小,就不會有這種下載馬上過時的狀況,好比 Cache-Control: max-age=2592000,s-maxage=3600,或者徹底不指定 s-maxage,Cache-Control: max-age=31536000 都行。那 max-age 比 s-maxage 小就徹底是錯的嗎?我覺的並非,我猜想這麼設置的理由是:這些圖片有更新的需求,因此給瀏覽器設置的緩存時間是一小時,給 CDN 設置的緩存時間是一年是由於更新圖片畢竟是小几率事件,不是大批量的,因此都是經過 CDN 提供的 purge 接口進行強制回源的,不須要 CDN 因資源過時發起大量主動回源。

因此我我的覺的 CDN 返回的 Date 響應頭應該使用 CDN 服務器的當前時間,而不是用緩存的陳舊的源站時間。

二. 大部分 JS/CSS 文件在頁面刷新時沒法發送條件請求

也就是沒法產生 304 響應。上篇文章已經說過了,Chrome 在頁面刷新時已經再也不爲子資源文件發送條件請求了(直接讀取緩存),但在國內,國產瀏覽器纔是王道,尤爲是移動端(沒幾個用 Chrome 的),目前這些瀏覽器仍然會在刷新時爲頁面中的 JS/CSS 文件發起條件請求。

若是你使用 Chrome 56(當前穩定版本)的話,須要把 chrome://flags/#enable-non-validating-reload-on-normal-reload 調成已停用(更高版本的 Chrome 已經沒有這個選項,請換個瀏覽器),再運行下面的 demo:

<div>刷新當前頁面,兩個 JS 文件都應該是 304 響應</div>
<script src="http://common.cnblogs.com/script/jquery.js"></script>
<script src="https://g.alicdn.com/kissy/k/1.4.2/seed-min.js"></script>

上面這個 demo 中有兩個 JS 文件,一個是淘寶 CDN 上的 KISSY,一個是博客園上的 jQuery。當你刷新頁面時,會發現 jQuery 這個請求的確是 304 響應,而 KISSY 這個每次是 200,也就是像徹底沒緩存同樣:

上面的視頻演示中,我爲了模擬較差的網絡環境,故意將網速節流成了 3G 模式。在 waterfall 列裏能夠看到,獲取完整響應的 200 請求比只拿響應頭的 304 請求多了 100 多毫秒的加載時間(藍色條部分)。考慮到如今的網頁沒有 JS/CSS 基本什麼都展示不了,因此這個問題會讓頁面刷新後的白屏時間大幅增長。

緣由是什麼?咱們看一下這個 JS 文件的響應頭(已經刪掉了 x- 開頭的):

$ curl -I 'https://g.alicdn.com/kissy/k/1.4.2/seed-min.js'

HTTP/1.1 200 OK

Server: Tengine

Content-Type: application/javascript

Content-Length: 44971

Connection: keep-alive

Date: Thu, 23 Feb 2017 05:50:47 GMT

Vary: Accept-Encoding

Accept-Ranges: bytes

Cache-Control: max-age=2592000,s-maxage=3600

Access-Control-Allow-Origin: *

Via: cache16.l2eu6-1[0,200-0,H], cache17.l2eu6-1[1,0], cache4.cn298[0,200-0,H], cache5.cn298[1,0]

Age: 2605

Timing-Allow-Origin: *

EagleId: 8ccd84cd14878316529776698e

問題就在,響應頭裏沒有 Last-Modified 和 ETag,所以瀏覽器無法生成 If-Modified-Since 和 If-None-Match 請求頭,因此無法發送條件請求,只能發個普通的非條件請求。

刷新並不算是極端狀況,好比移動端的下拉刷新,是很常見的,所以刷新的用戶體驗也是須要保障的。

相關文章
相關標籤/搜索