關(guān)于Web緩存的那些風(fēng)流事兒
最近大家針對(duì)preload、HTTP/2 push和ServiceWorker的瀏覽器緩存實(shí)現(xiàn)展開(kāi)了激烈的討論,而這也引起了很多人的疑惑。
鑒于此,我想講個(gè)故事來(lái)讓大家了解一個(gè)請(qǐng)求如何完成他的使命并找到匹配的緩存資源,以下內(nèi)容均基于 Chromium 的術(shù)語(yǔ),不過(guò)其余瀏覽器的實(shí)現(xiàn)本質(zhì)上沒(méi)有太大的差異。
Questy 的旅程
Questy 是一個(gè)請(qǐng)求。她是在渲染引擎內(nèi)(也叫渲染器)誕生的。她渴望能在這個(gè)標(biāo)簽頁(yè)關(guān)閉前找到一個(gè)讓她的“人生”再無(wú)遺憾的資源。
所以 Questy 展開(kāi)了她追求幸福的旅程。 但是她會(huì)在哪里找到一個(gè)恰恰適合的資源呢?
此時(shí)離她最近的是……
內(nèi)存緩存(Memory Cache)
內(nèi)存緩存中包含了大量的資源。他包含了所有渲染引擎請(qǐng)求的資源。這些資源都是現(xiàn)有文檔的一部分。在文檔的生命周期中他們都會(huì)被儲(chǔ)存在此。這意味著,如果 Questy 尋找的資源已經(jīng)被文檔中的其余部分加載了,那么他們會(huì)在此相遇。
確切來(lái)說(shuō),“短期內(nèi)存緩存”這個(gè)名字可能會(huì)更適合。因?yàn)閮?nèi)容緩存僅在導(dǎo)航結(jié)束前保存這些資源,在某些情況下,時(shí)間甚至?xí)獭?/p>
事實(shí)上,很多種情況都會(huì)導(dǎo)致 Questy 尋找的資源已經(jīng)被加載。
預(yù)加載器(preloader)可能是最常發(fā)生的情況。如果 Questy 是由 HTML 解析器創(chuàng)造的 DOM節(jié)點(diǎn)所激發(fā)的,那么她很可能會(huì)發(fā)現(xiàn),她所尋找的資源早已在 HTML 標(biāo)記化階段加載完畢了。
顯示 preload 指令()則是另一種較為可能發(fā)生的情況。該指令會(huì)讓瀏覽器預(yù)加載資源并存儲(chǔ)在內(nèi)存緩存中。
除此之外,還有可能是因?yàn)樗?qǐng)求的資源與之前的 DOM 節(jié)點(diǎn)或者 CSS 規(guī)則所需要的資源相同。例如,一個(gè)頁(yè)面中可能會(huì)含有多個(gè)具有相同 src 屬性的<img>元素,但是他們會(huì)得到同一個(gè)資源。而實(shí)現(xiàn)這種機(jī)制的正是內(nèi)存緩存。
然而,內(nèi)存緩存不會(huì)輕易匹配我們的資源請(qǐng)求。當(dāng)然了,為了使請(qǐng)求和資源相匹配,他們必須要有相同的 URL 。不過(guò),這還不是全部。他們還必須要有相同的資源類型(這樣子一個(gè)腳本資源才不會(huì)被一個(gè)圖片請(qǐng)求所匹配),相同的 CORS 策略和一些其他特性。
- 規(guī)范并沒(méi)有十分地明確定義內(nèi)存緩存所需要匹配的特性,所以不同的瀏覽器的實(shí)現(xiàn)可能會(huì)有一定的差異。
有一樣?xùn)|西是內(nèi)存緩存不關(guān)心的,那就是 HTTP 語(yǔ)義。無(wú)論資源的頭部是是否帶有 max-age=0 或者 no-cache 、Cache-Control標(biāo)簽,內(nèi)存緩存都不關(guān)心。因?yàn)樵诋?dāng)前導(dǎo)航中,資源是可以重用的,所以 HTTP 語(yǔ)義并不重要。
唯一例外的是no-store指令。在某些特定的情況下瀏覽器會(huì)尊重他。(例如,當(dāng)資源被單獨(dú)節(jié)點(diǎn)重用時(shí))。
所以,Questy 走上前詢問(wèn)內(nèi)存緩存是否有匹配的資源。唉,然而并沒(méi)有。
Questy 并沒(méi)有放棄。她走過(guò)資源計(jì)時(shí)器和開(kāi)發(fā)者工具的網(wǎng)絡(luò)注冊(cè)點(diǎn)。在那里,她注冊(cè)為尋找資源的請(qǐng)求(這意味著如果她能找到匹配的資源,則會(huì)出現(xiàn)在開(kāi)發(fā)中工具和資源計(jì)時(shí)器中)。
完成了這些官方登記后,她繼續(xù)向前……
Service Worker 緩存
和內(nèi)存緩存不一樣,Service Woker喜歡不走尋常路。他的行為難以預(yù)測(cè)。因?yàn)樗蛔裱_(kāi)發(fā)者告訴他的規(guī)則。
首先,Service Worker只有安裝后才會(huì)存在。而且因?yàn)樗倪壿嬍怯砷_(kāi)發(fā)者編寫(xiě)的 JavaScript 而不是瀏覽器控制的,所以 Questy 完全不知道她能不能在這里找到那個(gè)他?那個(gè)資源長(zhǎng)成什么的?他是被存儲(chǔ)在緩存里嗎?還是說(shuō)他是由 Service Worker 的主人精心偽造的響應(yīng)?
這些問(wèn)題沒(méi)有人可以回答她。因?yàn)?Service Worker 自成一套,無(wú)論是資源的匹配方式還是響應(yīng)的包裝方法,他們都能按照自己的的想法去完成。
Service Worker 擁有和緩存相關(guān)的 API ,這讓他可以儲(chǔ)存資源。和內(nèi)存儲(chǔ)存不同的是這種存儲(chǔ)方式是持久的。即使該標(biāo)簽頁(yè)被關(guān)閉甚至瀏覽器重啟,這些被存儲(chǔ)的資源都不會(huì)丟失。只有當(dāng)開(kāi)發(fā)者明確表示要移除他們的時(shí)候(使用 cache.delete(resource)),他們才會(huì)被移除。另外一種情況就是當(dāng)瀏覽器的存儲(chǔ)空間不足時(shí),他會(huì)將整個(gè) Service Worker 緩存還有其他源存儲(chǔ)如 indexedDB、localStorage 等都清除掉。也因此,Service Worker 能確保他的存儲(chǔ)和其他源存儲(chǔ)是同步的。
Service Worker 只負(fù)責(zé)特定的域,換言之,他最多只能管理一個(gè) host。因此,Service Worker 只能控制來(lái)自特定域內(nèi)的文檔的請(qǐng)求。
Questy 走向 Service Worker 詢問(wèn)他有沒(méi)有合適的資源??上У氖?Service Worker 從來(lái)沒(méi)有見(jiàn)過(guò)那個(gè)域的資源,所以他也找不到 Questy 尋找的請(qǐng)求了。于是,Service Worker 讓 Questy 繼續(xù)前行(通過(guò) fetch()),從而在網(wǎng)絡(luò)棧這片神奇的土地里繼續(xù)尋找她需要的資源。
而一旦進(jìn)入網(wǎng)絡(luò)棧,最容易找到資源的地方就是……
HTTP 緩存
HTTP 緩存(有時(shí)候也被他的朋友成為“磁盤(pán)緩存”)和 Questy 之前遇到過(guò)的緩存不太一樣。
一方面,他們的存儲(chǔ)是持久的,而且能被不同的會(huì)話甚至不同的網(wǎng)站重用。如果一個(gè)資源被一個(gè)網(wǎng)站下載了,他也可以被其他網(wǎng)站重用,
而另一方面,HTTP 緩存遵循 HTTP 語(yǔ)義(名字早已暗示了一切)。他樂(lè)于提供他認(rèn)為覺(jué)得是“新鮮”的資源(基于由響應(yīng)的緩存頭聲明的生命周期)、校驗(yàn)?zāi)切┬枰匦买?yàn)證的資源、并拒絕存儲(chǔ)那些它不應(yīng)該存儲(chǔ)的資源。
既然他是一個(gè)持久性的緩存,他也需要移除資源。但和 Service Worker 不一樣的事,他會(huì)在覺(jué)得他需要空間來(lái)存儲(chǔ)更重要或者會(huì)被更多人需要的資源時(shí),逐個(gè)移除那些舊資源。
HTTP 緩存擁有一個(gè)基于內(nèi)存的組件。他負(fù)責(zé)為請(qǐng)求匹配資源??墒且坏┵Y源匹配成功,它需要從磁盤(pán)中獲取資源內(nèi)容,這是一個(gè)較為昂貴的操作。
上文我們提到 HTTP 緩存遵循 HTTP 語(yǔ)義。這基本是正確的。除了一個(gè)例外情況,HTTP 緩存會(huì)存儲(chǔ)一些資源一段時(shí)間。瀏覽器能夠?yàn)橄麓螌?dǎo)航預(yù)取資源。我們可以通過(guò)顯示的指令()或者依靠瀏覽器內(nèi)部機(jī)制完成。這些被預(yù)取的資源會(huì)被保存下來(lái)直到下次導(dǎo)航,盡管它們可能是不允許緩存的。所以當(dāng)預(yù)取資源到達(dá) HTTP 緩存時(shí),它會(huì)被緩存(并且不需要校驗(yàn)就會(huì)被提供)大概五分鐘。
盡管 HTTP 緩存看起來(lái)十分的嚴(yán)厲,但 Questy 還是鼓起勇氣上前詢問(wèn)有沒(méi)有匹配的資源。然而答案依舊是沒(méi)有。
她還是得繼續(xù)隨著網(wǎng)絡(luò)往前走。這段旅程時(shí)可怕而且未知的,然而 Questy 知道無(wú)論如何她都要找到她需要的資源。所以她只能繼續(xù)。這時(shí)候她找到了一個(gè)對(duì)應(yīng)的 HTTP/2 會(huì)話。并且準(zhǔn)備通過(guò)網(wǎng)絡(luò)繼續(xù)前行,這時(shí)候她忽然看到了……
推送“緩存”
推送緩存(其實(shí)他更應(yīng)該被描述為“待認(rèn)領(lǐng)的推送流存儲(chǔ)器”,不過(guò)那實(shí)在是太拗口了)是存儲(chǔ) HTTP/2 推送資源的地方。它們是 HTTP/2 會(huì)話的一部分,這有幾個(gè)特殊的含義。
這個(gè)容器并不是持久的。當(dāng)會(huì)話結(jié)束后,未被認(rèn)領(lǐng)的資源(例如,從來(lái)沒(méi)有被請(qǐng)求匹配到的)就會(huì)被移除。如果資源是由不同的 HTTP/2 會(huì)話獲取的,他們并不會(huì)匹配。除此之外,推送緩存只會(huì)存儲(chǔ)資源一段時(shí)間(在基于 Chromium 的瀏覽器里,這個(gè)時(shí)長(zhǎng)約為五分鐘)。
推送緩存根據(jù)請(qǐng)求的 URL 和請(qǐng)求頭匹配相應(yīng),但他不遵循嚴(yán)格的 HTTP 語(yǔ)義。
- 規(guī)范里也沒(méi)有明確定義推送緩存,所以再各個(gè)瀏覽器、系統(tǒng)或者 HTTP/2 客戶端間的實(shí)現(xiàn)可能會(huì)不一樣。
盡管信心不大,Questy 還是上前詢問(wèn)是否有匹配的請(qǐng)求。令人驚訝的是,他真的有!!Questy 喜出望外的認(rèn)領(lǐng)了這個(gè)資源(這也意味著它將這個(gè) HTTP/2 流從待認(rèn)領(lǐng)容器中移除)?,F(xiàn)在她可以回去渲染這個(gè)資源了。
在他們回程的路上,他們走過(guò)了 HTTP 緩存,并且話費(fèi)了一些時(shí)間去復(fù)制了一份資源以備日后使用。
離開(kāi)網(wǎng)絡(luò)棧后,他們回到 Service Worker 的轄區(qū),而 Service Worker 也將一份資源的拷貝存儲(chǔ)到自己的緩存中才讓他們回到渲染器里。
最終,一旦它們會(huì)到渲染器,內(nèi)存緩存就會(huì)保存一份資源的引用(而不是拷貝)。這樣子在稍后如果在同一個(gè)導(dǎo)航會(huì)話中需要這份資源,他就可以將相同的資源分配給他。
于是,它們就幸??旎畹淖≡诹艘黄穑钡轿臋n被移除,然后他們都被垃圾回收了。
不過(guò)那是另外一天的故事了。
要點(diǎn)
所以,從 Questy 旅程中我們能學(xué)習(xí)到什么呢?
- 不同的請(qǐng)求可以從不同的瀏覽器緩存中匹配的資源。
- 請(qǐng)求匹配資源的緩存的不同會(huì)影響這個(gè)請(qǐng)求是否會(huì)被開(kāi)發(fā)者工具和資源計(jì)時(shí)器所展示。
- 推送資源不會(huì)被持續(xù)存儲(chǔ)除非他們被請(qǐng)求所認(rèn)領(lǐng)。
- 不能存儲(chǔ)的預(yù)加載資源在下一個(gè)導(dǎo)航時(shí)不會(huì)存在。這是預(yù)加載(preload)和預(yù)取(prefetch)間的最大區(qū)別。
- 因?yàn)檫€有很多地方規(guī)范沒(méi)有明確定義,所以不同的瀏覽器實(shí)現(xiàn)會(huì)有差異。我們需要彌補(bǔ)這些差異。
總而言之,如果你使用預(yù)加載,HTTP/2 推送, Service Worker 又或者其他高級(jí)技術(shù)來(lái)加速你的網(wǎng)站,你可能會(huì)注意到內(nèi)部緩存的實(shí)現(xiàn)情況。了解這些內(nèi)部緩存和他們的運(yùn)作方式能讓你更好的解決問(wèn)題并且減少不必要的麻煩。