文章出處

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

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

淘寶網站上什么類型的請求最多?當然是圖片了。拿淘寶首頁舉例,在 Chrome 的新標簽頁中先打開開發者工具,再打開淘寶首頁,然后滾動到頁面最底部,在開發者工具的網絡面板中點擊 Img 篩選條件后能夠看到左下角有類似如下的數字:

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

<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 的請求):

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

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

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

3. 頁面中直接讀取緩存的圖片絲毫不動,而經過 304 后再讀取緩存的圖片有明顯的閃動。

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

原因是什么?讓我們看看這張圖片的響應頭(已經刪掉了 x- 開頭的):

$ 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 請求頭,所以沒法發送條件請求,只能發個普通的非條件請求。

刷新并不算是極端情況,比如移動端的下拉刷新,是很常見的,因此刷新的用戶體驗也是需要保障的。

 


文章列表




Avast logo

Avast 防毒軟體已檢查此封電子郵件的病毒。
www.avast.com


arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()