1. 程式人生 > >最常被遺忘的 Web 效能優化:瀏覽器快取

最常被遺忘的 Web 效能優化:瀏覽器快取

一提起快取, Web開發者們總是在想資料庫快取、頁面靜態化、使用 Redis記憶體快取。這些方法都有一個共性,就是集中在後臺,目的就是加快資料的讀取,少用比較容易產生瓶頸的部分。

後臺該優化的都優化到了最佳狀態,卻往往疏忽了一個非常重要的過程,就是資料傳輸。想著如何快速讀取資料,卻忘了如何減少請求資料,或者根本不請求資料。所以,今天我們就來聊一聊這個經常被我們遺忘的瀏覽器快取。

認識瀏覽器快取

當瀏覽器請求一個網站的時候,會載入各種各樣的資源,比如 HTML文件、圖片、 CSS和 JS等檔案。對於一些不經常變的內容,瀏覽器會將他們儲存在本地的檔案中,下次訪問相同網站的時候,直接載入這些資源,加速訪問。

這些被瀏覽器儲存的檔案就被稱為快取。(不是指 Cookie或者 Localstorage)。

那麼如何知曉瀏覽器是讀取了快取還是直接請求伺服器?我們就使用 Segmentfault網站來做個示例(見下圖)。

第一次開啟該網站後,如果再次重新整理頁面。會發現瀏覽器載入的眾多資源中,有一部分 size有具體數值,然而還有一部分請求,比如圖片、 css和 js等檔案並沒有顯示檔案大小,而是顯示了 fromdis cache或者 frommemory cache字樣。這就說明了,該資源直接從記憶體或者本地硬碟直接讀取,而並沒有請求伺服器。

檢視快取

知道了瀏覽器從快取中讀取檔案,那麼瀏覽器快取檔案儲存在哪裡?以 chrome

為例,直接在瀏覽器位址列輸入: chrome://cache/即可開啟近期的所有快取檔案連結,當然你可以直接點選開啟快取內容。

至於背後的檔案,一般存在於: C:UsersyanyingAppDataLocalGoogleChromeUserDataDefaultCache路徑中,其中 yanying是你的 windows使用者名稱稱。

快取協商

從上面的圖片可以看出。一部分請求使用了快取,而有一部分快取並沒有使用快取。瀏覽器如果想判斷何時該做什麼操作,就必須要有一個判定標準。這裡就需要用到快取協商。簡單來說就是 Web瀏覽器和伺服器之間協定一個法則,什麼情況下請求資源,什麼情況下不請求。

快取協商方式和 Cookie

、 User-Agent一樣,通過瀏覽器 header進行傳輸。

快取協商方式有3種:

  1. Last-Modified

  2. ETag

  3. Expires

Last-modified 定義

Last-Modified標籤代表是檔案的最後修改時間,其格式是標準的 GMT時間。注意:GMT是標準的格林威治時間,我們國家是 GMT+8時區。所以,你看到的 Last-Modified和我們的時間有8個小時差距,不過不影響使用。

一般的動態資源沒有所謂的最後修改時間。而靜態檔案比如 css檔案、圖片等檔案可以通過 stat()系統呼叫獲得檔案的最後修改時間。

但是,實際網站執行中, Web伺服器(比如 Apache)會自動獲取靜態資源的最後修改時間,同時會自動在 HTTP標頭檔案中新增 Last-Modified標籤。靜態資源的相應標頭檔案如下圖所示:

包含了 Last-Modified標籤的資源,在下次的請求中,瀏覽器會帶著該時間。當伺服器接收到請求後會核對該時間後,檔案是否被修改,如果修改了就直接返回資料,沒有修改就直接返回 304狀態碼,告知瀏覽器直接使用本地快取。這樣,一次資料傳輸流量就被免除了,速度稍有加快。

動態資源中使用

動態資源雖然沒有相對意義上的最後修改時間,但是我們還是可以直接通過傳送 header頭來手動定義 Last-Modified。這樣,通過動態程式判斷,也可以達到靜態資源節省資料傳輸流量的作用。

這裡使用 PHP舉個例子:

1、首先建立一個 php檔案,傳送一個 Last-Modified頭標籤:

  1. <?php

  2. header("Last-Modified:".gmdate("D, d M Y H:i:s")." GMT");

2、使用瀏覽器請求該檔案,我們得到了如下的伺服器返回頭:

  1. HTTP/1.1200OK

  2. Date:Tue,27Jun201715:13:02GMT

  3. Server:Apache/2.4.9(Win32)PHP/5.5.12

  4. X-Powered-By:PHP/5.5.12

  5. Last-Modified:Tue,27Jun201715:13:02GMT

  6. Content-Length:0

  7. Keep-Alive:timeout=5,max=97

  8. Connection:Keep-Alive

  9. Content-Type:text/html

觀察上面伺服器返回的標頭檔案,包含了一個 Last-Modified:Tue,27Jun201715:13:02GMT,這就是上面動態程式碼生成的最後修改時間。

3、當我們再次請求該檔案的時候,我們看下瀏覽器傳送給伺服器的標頭檔案。

  1. GET /php/last.php HTTP/1.1

  2. Host:localhost

  3. Connection:keep-alive

  4. Cache-Control:max-age=0

  5. //...這裡省略部分資訊

  6. If-Modified-Since:Tue,27Jun201715:13:02GMT

觀察一下最後一行,多了一個 If-Modified-Since標籤,他的時間正是伺服器剛剛返回的 Last-Modified的值。這個值就這樣又被返回給了伺服器。

4、這樣就很簡單啦。在動態語言端( PHP)可以直接使用 $_SERVER['HTTP_IF_MODIFIED_SINCE']即可獲取時間值,接著就可以做一些簡單的對比工作。如果在這個時間之後資料沒有變化則直接返回 304,告訴瀏覽器直接使用快取,而免去資料傳輸的過程。

而且,最終要的是。這個過程根本無需查詢資料庫,所以後臺程式執行時間非常短,從而大大減少使用者等待時間。

這樣我們就做到了動態資源也可以實現靜態資源的最後修改時間,從而減少資料傳輸量,達到優化效能要求。

Etag Last-Modified缺點

Last-Modified似乎已經做到了部分效能優化效果。但是,總是有些情況下不是很奏效。比如,一個使用者修改了一個檔案,後來使用者覺得修改錯誤,於是又修改回去。

上面的過程中,檔案內容並沒有發生變化。但是,檔案在系統中的物理最後修改時間卻發生了變化。這種情況下,如果瀏覽器再次請求資源。伺服器還是會發送完整資料。從而並未完全達到我們預想的效果。

於是在此之上,我們還可以新增一個 ETag標籤,用來進一步確認檔案是否修改。

瞭解ETag

ETag類似於 Last-Modified,也是一個 header頭標籤。他的值是一串字串,用於區分各個檔案的版本資訊,由於 HTTP並沒有對該值做任何的格式限制,所以可以自定義生成。

ETag的值不同於 Last-Modified,他並不會在檔案被修改時候就發生變化,而是在檔案內容發生變化的時候才會被改變(具體什麼時候改變,完全有後臺業務邏輯來判斷)。對於靜態資源, Web伺服器還是會幫我們處理好這個標籤,不用考慮太多。

這裡我們截取了 Segmentfault的一張圖片的 ETag,如下圖:

下面我們還是來討論一下動態資源模擬靜態資源傳送 ETag標籤的過程:

1、這裡我們還是新建一個 PHP檔案,其中程式碼是向瀏覽器傳送一個包含 ETag的標頭檔案。

  1. <?php

  2. header("ETag : abcd");

2、使用瀏覽器請求該 PHP檔案:

看下伺服器返回的 header頭:

  1. HTTP/1.1200OK

  2. Date:Wed,28Jun201701:45:40GMT

  3. Server:Apache/2.4.9(Win64)PHP/5.5.12

  4. X-Powered-By:PHP/5.5.12

  5. ETag:abcd

  6. Content-Length:0

  7. Keep-Alive:timeout=5,max=100

  8. Connection:Keep-Alive

  9. Content-Type:text/html

裡面比正常的返回多了一個 ETag標籤,並且它的值就是我們剛剛設定的 abcd

3、下面我們重新整理瀏覽器,再次請求改頁面:

注意觀察下瀏覽器請求的頭 header

  1. GET /etags.php HTTP/1.1

  2. Host:localhost

  3. Connection:keep-alive

  4. Cache-Control:max-age=0

  5. Upgrade-Insecure-Requests:1

  6. User-Agent:Mozilla/5.0(WindowsNT 10.0;Win64;x64)AppleWebKit/537.36(KHTML,like Gecko)Chrome/59.0.3071.86Safari/537.36

  7. Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8

  8. Accept-Encoding: gzip, deflate, br

  9. Accept-Language: zh-CN,zh;q=0.8

  10. Cookie: Phpstorm-65418376=dceeb07b-c7af-45d6-b8be-4079e9424244; Hm_lvt_65dfcf8f1948f7203dd3fb620de01083=1497600508; admin_id=1; admin_token=072517cddaa9c106fe4662ea70a1345c

  11. If-None-Match: abcd

仔細看下最後一行,有一個 If-None-Match頭標籤。該標籤的值正是我們剛剛接收到的伺服器返回的 ETag的值,這樣類似於 Last-Modified,我們在 PHP端可以使用 $_SERVER['HTTP_IF_NONE_MATCH']直接獲取我們剛剛的值。

4、獲取到該值,我們就可以直接對比檔案現有的 ETag,來決定是直接使用瀏覽器快取還是再次傳送完整資料。

小結

Etag和 Last-Modified非常相似,都是用來判斷一個引數,從而決定是否啟用快取。但是 ETag相對於 Last-Modified也有他的優勢,他可以更加準確的判斷檔案內容是否被修改,從而在實際操作中實用程度也更高。

有了這兩種優化方式,對於節省流量頻寬已經起到了非常大的作用。但是總是感覺還是有點兒雞肋,畢竟每次瀏覽器還是要來詢問一下伺服器,檔案是否被改變。

如果,我們可以確定,一個檔案在半年內不會改變,那麼我們可以讓瀏覽器在這半年時間內都不來伺服器詢問,而直接使用本地快取。這裡就需要使用第三種協商方式 Expires.

Expires

Expires這個單詞的意思是過期,在這裡表示的是過期時間。它的使用方式、格式和 Last-Modified一樣,都是使用瀏覽器頭,也都是標準的 GMT時間。

但是它的功能卻完全不同,包含了 Expires頭標籤的檔案,就說明瀏覽器對於該檔案快取具有非常大的控制權。例如,一個檔案的 Expires值是2020年的1月1日,那麼就代表,在2020年1月1日之前,瀏覽器都可以直接使用該檔案的本地快取檔案,而不必去伺服器再次請求該檔案,哪怕伺服器檔案發生了變化。

所以, Expires是優化中最理想的情況,因為它根本不會產生請求,所以後端也就無需考慮查詢快慢。

下面我們看下 segmentfault的靜態檔案的 Expires

對於靜態資源,大多數伺服器是會開啟 expires標記功能。如果遇到沒有開啟的,則可以使用配置檔案開啟。

Apache的 expires支援設定如下:

  1. <IfModulemod_expires.c>

  2. ExpiresActive on

  3. ExpiresByType image/gif "access plus 1 month"

  4. ExpiresByType text/css "now plus 2 day"

  5. ExpiresDefault "now plus 1 day"

  6. </IfModule>

上面的配置中我們設定 image/gif的格式圖片快取時間為1個月,而 css檔案快取時間為2天,其他的預設為1天。

另外,對於常用靜態資源。如果不在 web伺服器端設定 expires標籤,瀏覽器也可以智慧的標記一個過期時間。比如 gif圖片,瀏覽器會設定他的過期時間為永不過期。

動態資源中使用Expires

這裡我們還是拿 PHP來舉例

1、首先建立一個 PHP檔案,用於傳送 Expires頭標籤。

這裡我們把檔案過期時間直接設定為2020年1月1日的0點

  1. <?php

  2. header("Expires:".gmdate("D, d M Y H:i:s",1577808000)." GMT");

2、使用瀏覽器請求該檔案,觀察伺服器返回的標頭檔案:

  1. HTTP/1.1200OK

  2. Date:Wed,28Jun201702:24:18GMT

  3. Server:Apache/2.4.9(Win64)PHP/5.5.12

  4. X-Powered-By:PHP/5.5.12

  5. Expires:Tue,31Dec201916:00:00GMT

  6. Content-Length:0

  7. Keep-Alive:timeout=5,max=100

  8. Connection:Keep-Alive

  9. Content-Type:text/html

不出意外,我們已經在標頭檔案裡面發現 Expires標籤,並且它的值為 Tue,31Dec201916:00:00GMT(這裡不是2020年原因是由於有8個小時時差)。

3、再次使用瀏覽器訪問改頁面,發現瀏覽器的請求資料的路徑已經變為了 fromcache(如下圖)。

對於 chrome瀏覽器,從 network中似乎並不能看出是否使用了快取。但是,如果開啟 chrome://cache/,搜尋我們剛剛的地址,會發現我們請求的內容也被快取成功。

請求方式與快取

瀏覽器有3種請求伺服器資源的方式

  1. ctrl+f5:強制重新整理

  2. f5:重新整理頁面

  3. 瀏覽器位址列回車,也就是轉到功能

這3種請求方式對於資源使用快取的影響各不不同,下面一一的解釋:

1、ctrl + f5:強制重新整理

這種方式是所有載入方式中使用快取最少的方式。當使用 ctrl+f5訪問一個地址的時候,瀏覽器會強制所有的資源重新載入一次。所有的資源將會被重新快取。

2、f5:重新整理頁面

f5重新整理頁面相當於瀏覽器上面的重新整理按鈕,是一種比較常用的重新整理方式。這種方式下瀏覽器會使用部分必要的快取,針對於 Last-Modified有效,但是 expires標籤就會失去他的作用。

3、位址列轉到方式

在瀏覽器位址列輸入即將訪問的地址後,按回車或者瀏覽器轉到功能訪問網頁。這是使用最多的一種情況,也是使用最少請求伺服器的方式。也就說瀏覽器會盡量使用本地快取,而避免直接請求伺服器資料。注意:Expires標籤也只有在這種情況下有效。所以,千萬不要使用 f5或者 ctrl+f5還奇怪 expires功能無效。

cache-control 還有一點點小缺陷

瞭解了上面所有的快取協商方式後,我們已經可以高效的優化我們現有的應用。但是還是存在一種可能情況,那就是之前的 Last-Modified和 expires都是使用伺服器標準時間來標記。

而作為最後的判斷者確是瀏覽器。所以,難免會存在使用者電腦時間和伺服器時間不一致的情況。

比如我們設定一個資源在未來10分鐘內不會過期,而使用者電腦比伺服器時間快了1個小時(當然這個太少見)。那麼我們設定的過期時間對於使用者來講,立即就過期了。那麼我們的設定相當於白用功了。

所以為了解決這個可能出現的小缺陷,我們還可以設定一個相對於使用者本地時間的快取過期時間 cache-control

作用

cache-control和之前的 Last-Modified一樣,都是標頭檔案裡面的一個標籤。只不過他的值是 max-age=<second>,這裡的 <second>是一個數字,單位為秒。

假設我們設定一個值 cahce-control:max-age=3600,那麼就代表改快取有效期是使用者本地時間加上 3600秒。這樣,快取的截止時間就和伺服器時間沒有太大關係了,從而避免了因為時間偏差帶來的不良影響。

對於靜態檔案,如果伺服器比如 Apache開啟了 expires功能,那麼也會預設的給標頭檔案新增一個 cache-control標籤。

PHP設定cache-control

對於動態檔案,我們可以在程式語言中向瀏覽器直接輸出該標籤。我們使用 PHP做一個演示:

1、建立一個 PHP檔案,向瀏覽器輸出一個包含 cache-control標籤的頭:

  1. <?php

  2. header("Cache-Control:max-age=3600");

2、使用瀏覽器請求該 PHP檔案,獲取伺服器返回頭 header

  1. HTTP/1.1200OK

  2. Date:Wed,28Jun201712:33:16GMT

  3. Server:Apache/2.4.9(Win32)PHP/5.5.12

  4. X-Powered-By:PHP/5.5.12

  5. Cache-Control:max-age=3600

  6. Content-Length:0

  7. Keep-Alive:timeout=5,max=98

  8. Connection:Keep-Alive

  9. Content-Type:text/html

觀察上面的資訊,可以發現其中包含 cache-control標籤,其值為我們剛剛設定的 max-age=3600,那麼就代表相對於我本地時間 3600秒之後快取過期。(完)