浏覽器緩存,也就是用戶端緩存,既是網頁性能優化裡面靜态資源相關優化的一大利器,也是無數web開發人員在工作過程不可避免的一大問題,是以在産品開發的時候我們總是想辦法避免緩存産生,而在産品釋出之時又在想政策管理緩存提升網頁的通路速度。了解浏覽器的緩存命中原理,是開發web應用的基礎,本文着眼于此,學習浏覽器緩存的相關知識,總結緩存避免和緩存管理的方法,結合具體的場景說明緩存的相關問題。希望能對有需要的人有所幫助。
1. 浏覽器緩存基本認識
它分為強緩存和協商緩存:
1)浏覽器在加載資源時,先根據這個資源的一些http header判斷它是否命中強緩存,強緩存如果命中,浏覽器直接從自己的緩存中讀取資源,不會發請求到伺服器。比如某個css檔案,如果浏覽器在加載它所在的網頁時,這個css檔案的緩存配置命中了強緩存,浏覽器就直接從緩存中加載這個css,連請求都不會發送到網頁所在伺服器;
2)當強緩存沒有命中的時候,浏覽器一定會發送一個請求到伺服器,通過伺服器端依據資源的另外一些http header驗證這個資源是否命中協商緩存,如果協商緩存命中,伺服器會将這個請求傳回,但是不會傳回這個資源的資料,而是告訴用戶端可以直接從緩存中加載這個資源,于是浏覽器就又會從自己的緩存中去加載這個資源;
3)強緩存與協商緩存的共同點是:如果命中,都是從用戶端緩存中加載資源,而不是從伺服器加載資源資料;差別是:強緩存不發請求到伺服器,協商緩存會發請求到伺服器。
4)當協商緩存也沒有命中的時候,浏覽器直接從伺服器加載資源資料。
2. 強緩存的原理
當浏覽器對某個資源的請求命中了強緩存時,傳回的http狀态為200,在chrome的開發者工具的network裡面size會顯示為from cache,比如京東的首頁裡就有很多靜态資源配置了強緩存,用chrome打開幾次,再用f12檢視network,可以看到有不少請求就是從緩存中加載的:

強緩存是利用Expires或者Cache-Control這兩個http response header實作的,它們都用來表示資源在用戶端緩存的有效期。
Expires是http1.0提出的一個表示資源過期時間的header,它描述的是一個絕對時間,由伺服器傳回,用GMT格式的字元串表示,如:Expires:Thu, 31 Dec 2037 23:55:55 GMT,它的緩存原理是:
1)浏覽器第一次跟伺服器請求一個資源,伺服器在傳回這個資源的同時,在respone的header加上Expires的header,如:
2)浏覽器在接收到這個資源後,會把這個資源連同所有response header一起緩存下來(是以緩存命中的請求傳回的header并不是來自伺服器,而是來自之前緩存的header);
3)浏覽器再請求這個資源時,先從緩存中尋找,找到這個資源後,拿出它的Expires跟目前的請求時間比較,如果請求時間在Expires指定的時間之前,就能命中緩存,否則就不行。
4)如果緩存沒有命中,浏覽器直接從伺服器加載資源時,Expires Header在重新加載的時候會被更新。
Expires是較老的強緩存管理header,由于它是伺服器傳回的一個絕對時間,在伺服器時間與用戶端時間相差較大時,緩存管理容易出現問題,比如随意修改下用戶端時間,就能影響緩存命中的結果。是以在http1.1的時候,提出了一個新的header,就是Cache-Control,這是一個相對時間,在配置緩存的時候,以秒為機關,用數值表示,如:Cache-Control:max-age=315360000,它的緩存原理是:
1)浏覽器第一次跟伺服器請求一個資源,伺服器在傳回這個資源的同時,在respone的header加上Cache-Control的header,如:
2)浏覽器在接收到這個資源後,會把這個資源連同所有response header一起緩存下來;
3)浏覽器再請求這個資源時,先從緩存中尋找,找到這個資源後,根據它第一次的請求時間和Cache-Control設定的有效期,計算出一個資源過期時間,再拿這個過期時間跟目前的請求時間比較,如果請求時間在過期時間之前,就能命中緩存,否則就不行。
4)如果緩存沒有命中,浏覽器直接從伺服器加載資源時,Cache-Control Header在重新加載的時候會被更新。
Cache-Control描述的是一個相對時間,在進行緩存命中的時候,都是利用用戶端時間進行判斷,是以相比較Expires,Cache-Control的緩存管理更有效,安全一些。
這兩個header可以隻啟用一個,也可以同時啟用,當response header中,Expires和Cache-Control同時存在時,Cache-Control優先級高于Expires:
3. 強緩存的管理
前面介紹的是強緩存的原理,在實際應用中我們會碰到需要強緩存的場景和不需要強緩存的場景,通常有2種方式來設定是否啟用強緩存:
1)通過代碼的方式,在web伺服器傳回的響應中添加Expires和Cache-Control Header;
2)通過配置web伺服器的方式,讓web伺服器在響應資源的時候統一添加Expires和Cache-Control Header。
比如在javaweb裡面,我們可以使用類似下面的代碼設定強緩存:
java.util.Date date = new java.util.Date();
response.setDateHeader("Expires",date.getTime()+20000); //Expires:過時期限值
response.setHeader("Cache-Control", "public"); //Cache-Control來控制頁面的緩存與否,public:浏覽器和緩存伺服器都可以緩存頁面資訊;
response.setHeader("Pragma", "Pragma"); //Pragma:設定頁面是否緩存,為Pragma則緩存,no-cache則不緩存
還可以通過類似下面的java代碼設定不啟用強緩存:
response.setHeader( "Pragma", "no-cache" );
response.setDateHeader("Expires", 0);
response.addHeader( "Cache-Control", "no-cache" );//浏覽器和緩存伺服器都不應該緩存頁面資訊
tomcat還提供了一個ExpiresFilter專門用來配置強緩存,具體使用的方式可參考tomcat的官方文檔:http://tomcat.apache.org/tomcat-7.0-doc/config/filter.html#Expires_Filter
nginx和apache作為專業的web伺服器,都有專門的配置檔案,可以配置expires和cache-control,這方面的知識,如果你對運維感興趣的話,可以在百度上搜尋“nginx 設定 expires cache-control”或“apache 設定 expires cache-control”都能找到不少相關的文章。
由于在開發的時候不會專門去配置強緩存,而浏覽器又預設會緩存圖檔,css和js等靜态資源,是以開發環境下經常會因為強緩存導緻資源沒有及時更新而看不到最新的效果,解決這個問題的方法有很多,常用的有以下幾種:
1)直接ctrl+f5,這個辦法能解決頁面直接引用的資源更新的問題;
2)使用浏覽器的隐私模式開發;
3)如果用的是chrome,可以f12在network那裡把緩存給禁掉(這是個非常有效的方法):
4)在開發階段,給資源加上一個動态的參數,如css/index.css?v=0.0001,由于每次資源的修改都要更新引用的位置,同時修改參數的值,是以操作起來不是很友善,除非你是在動态頁面比如jsp裡開發就可以用伺服器變量來解決(v=${sysRnd}),或者你能用一些前端的建構工具來處理這個參數修改的問題;
5)如果資源引用的頁面,被嵌入到了一個iframe裡面,可以在iframe的區域右鍵單擊重新加載該頁面,以chrome為例:
6)如果緩存問題出現在ajax請求中,最有效的解決辦法就是ajax的請求位址追加随機數;
7)還有一種情況就是動态設定iframe的src時,有可能也會因為緩存問題,導緻看不到最新的效果,這時候在要設定的src後面添加随機數也能解決問題;
8)如果你用的是grunt和gulp這種前端工具開發,通過它們的插件比如grunt-contrib-connect來啟動一個靜态伺服器,則完全不用擔心開發階段的資源更新問題,因為在這個靜态伺服器下的所有資源傳回的respone header中,cache-control始終被設定為不緩存:
4. 強緩存的應用
強緩存是前端性能優化最有力的工具,沒有之一,對于有大量靜态資源的網頁,一定要利用強緩存,提高響應速度。通常的做法是,為這些靜态資源全部配置一個逾時時間超長的Expires或Cache-Control,這樣使用者在通路網頁時,隻會在第一次加載時從伺服器請求靜态資源,其它時候隻要緩存沒有失效并且使用者沒有強制重新整理的條件下都會從自己的緩存中加載,比如前面提到過的京東首頁緩存的資源,它的緩存過期時間都設定到了2026年:
然而這種緩存配置方式會帶來一個新的問題,就是釋出時資源更新的問題,比如某一張圖檔,在使用者通路第一個版本的時候已經緩存到了使用者的電腦上,當網站釋出新版本,替換了這個圖檔時,已經通路過第一個版本的使用者由于緩存的設定,導緻在預設的情況下不會請求伺服器最新的圖檔資源,除非他清掉或禁用緩存或者強制重新整理,否則就看不到最新的圖檔效果。
這個問題已經有成熟的解決方案,具體内容可閱讀知乎這篇文章詳細了解:
http://www.zhihu.com/question/20790576
文章提到的東西都屬于理論上的解決方案,不過現在已經有很多前端工具能夠實際地解決這個問題,由于每個工具涉及到的内容細節都有很多,本文沒有辦法一一深入介紹。有興趣的可以去了解下grunt gulp webpack fis 還有edp這幾個工具,基于這幾個工具都能解決這個問題,尤其是fis和edp是百度推出的前端開發平台,有現成的文檔可以參考:
http://fis.baidu.com/fis3/api/index.html
http://ecomfe.github.io/edp/doc/initialization/install/
強緩存還有一點需要注意的是,通常都是針對靜态資源使用,動态資源需要慎用,除了服務端頁面可以看作動态資源外,那些引用靜态資源的html也可以看作是動态資源,如果這種html也被緩存,當這些html更新之後,可能就沒有機制能夠通知浏覽器這些html有更新,尤其是前後端分離的應用裡,頁面都是純html頁面,每個通路位址可能都是直接通路html頁面,這些頁面通常不加強緩存,以保證浏覽器通路這些頁面時始終請求伺服器最新的資源。
5. 協商緩存的原理
當浏覽器對某個資源的請求沒有命中強緩存,就會發一個請求到伺服器,驗證協商緩存是否命中,如果協商緩存命中,請求響應傳回的http狀态為304并且會顯示一個Not Modified的字元串,比如你打開京東的首頁,按f12打開開發者工具,再按f5重新整理頁面,檢視network,可以看到有不少請求就是命中了協商緩存的:
檢視單個請求的Response Header,也能看到304的狀态碼和Not Modified的字元串,隻要看到這個就可說明這個資源是命中了協商緩存,然後從用戶端緩存中加載的,而不是伺服器最新的資源:
協商緩存是利用的是【Last-Modified,If-Modified-Since】和【ETag、If-None-Match】這兩對Header來管理的。
【Last-Modified,If-Modified-Since】的控制緩存的原理是:
1)浏覽器第一次跟伺服器請求一個資源,伺服器在傳回這個資源的同時,在respone的header加上Last-Modified的header,這個header表示這個資源在伺服器上的最後修改時間:
2)浏覽器再次跟伺服器請求這個資源時,在request的header上加上If-Modified-Since的header,這個header的值就是上一次請求時傳回的Last-Modified的值:
3)伺服器再次收到資源請求時,根據浏覽器傳過來If-Modified-Since和資源在伺服器上的最後修改時間判斷資源是否有變化,如果沒有變化則傳回304 Not Modified,但是不會傳回資源内容;如果有變化,就正常傳回資源内容。當伺服器傳回304 Not Modified的響應時,response header中不會再添加Last-Modified的header,因為既然資源沒有變化,那麼Last-Modified也就不會改變,這是伺服器傳回304時的response header:
4)浏覽器收到304的響應後,就會從緩存中加載資源。
5)如果協商緩存沒有命中,浏覽器直接從伺服器加載資源時,Last-Modified Header在重新加載的時候會被更新,下次請求時,If-Modified-Since會啟用上次傳回的Last-Modified值。
【Last-Modified,If-Modified-Since】都是根據伺服器時間傳回的header,一般來說,在沒有調整伺服器時間和篡改用戶端緩存的情況下,這兩個header配合起來管理協商緩存是非常可靠的,但是有時候也會伺服器上資源其實有變化,但是最後修改時間卻沒有變化的情況,而這種問題又很不容易被定位出來,而當這種情況出現的時候,就會影響協商緩存的可靠性。是以就有了另外一對header來管理協商緩存,這對header就是【ETag、If-None-Match】。它們的緩存管理的方式是:
1)浏覽器第一次跟伺服器請求一個資源,伺服器在傳回這個資源的同時,在respone的header加上ETag的header,這個header是伺服器根據目前請求的資源生成的一個唯一辨別,這個唯一辨別是一個字元串,隻要資源有變化這個串就不同,跟最後修改時間沒有關系,是以能很好的補充Last-Modified的問題:
2)浏覽器再次跟伺服器請求這個資源時,在request的header上加上If-None-Match的header,這個header的值就是上一次請求時傳回的ETag的值:
3)伺服器再次收到資源請求時,根據浏覽器傳過來If-None-Match和然後再根據資源生成一個新的ETag,如果這兩個值相同就說明資源沒有變化,否則就是有變化;如果沒有變化則傳回304 Not Modified,但是不會傳回資源内容;如果有變化,就正常傳回資源内容。與Last-Modified不一樣的是,當伺服器傳回304 Not Modified的響應時,由于ETag重新生成過,response header中還會把這個ETag傳回,即使這個ETag跟之前的沒有變化:
6. 協商緩存的管理
協商緩存跟強緩存不一樣,強緩存不發請求到伺服器,是以有時候資源更新了浏覽器還不知道,但是協商緩存會發請求到伺服器,是以資源是否更新,伺服器肯定知道。大部分web伺服器都預設開啟協商緩存,而且是同時啟用【Last-Modified,If-Modified-Since】和【ETag、If-None-Match】,比如apache:
如果沒有協商緩存,每個到伺服器的請求,就都得傳回資源内容,這樣伺服器的性能會極差。
【Last-Modified,If-Modified-Since】和【ETag、If-None-Match】一般都是同時啟用,這是為了處理Last-Modified不可靠的情況。有一種場景需要注意:
分布式系統裡多台機器間檔案的Last-Modified必須保持一緻,以免負載均衡到不同機器導緻比對失敗;
分布式系統盡量關閉掉ETag(每台機器生成的ETag都會不一樣);
京東頁面的資源請求,傳回的repsones header就隻有Last-Modified,沒有ETag:
協商緩存需要配合強緩存使用,你看前面這個截圖中,除了Last-Modified這個header,還有強緩存的相關header,因為如果不啟用強緩存的話,協商緩存根本沒有意義。
7. 浏覽器行為對緩存的影響
如果資源已經被浏覽器緩存下來,在緩存失效之前,再次請求時,預設會先檢查是否命中強緩存,如果強緩存命中則直接讀取緩存,如果強緩存沒有命中則發請求到伺服器檢查是否命中協商緩存,如果協商緩存命中,則告訴浏覽器還是可以從緩存讀取,否則才從伺服器傳回最新的資源。這是預設的處理方式,這個方式可能被浏覽器的行為改變:
1)當ctrl+f5強制重新整理網頁時,直接從伺服器加載,跳過強緩存和協商緩存;
2)當f5重新整理網頁時,跳過強緩存,但是會檢查協商緩存;
謝謝閱讀:)希望本文的内容能對你有所幫助~
如果您覺得本文對你有用,不妨幫忙點個贊,或者在評論裡給我一句贊美,小小成就都是今後繼續為大家編寫優質文章的動力,流雲拜謝!
歡迎您持續關注我的部落格:)
作者:流雲諸葛
出處:http://www.cnblogs.com/lyzg/
版權所有,歡迎保留原文連結進行轉載:)