過去兩年以來 ,Facebook 網站一直與瀏覽器廠家合作來改善瀏覽器快取效能。最近 Chrome 和 Firefox 推出的新功能,使其快取效能對於我們以及整個Web 而言都得到了顯著提升。這些功能已讓使用者訪問我們伺服器靜態資源的網路請求降低了 60%,同時極大地改善了頁面載入時間。(靜態資源是那些需要伺服器從磁碟讀取的檔案,而後僅僅提供它,而無需執行額外程式碼。)本文將詳細介紹我們在 Chrome 和 Firefox 中所做的努力並最終獲得上述結果 — 但是我們要先從一些上下文和定義開始,來弄清需解決的問題。 首先,重新驗證。
每個重新驗證意味著一個新的請求
當使用者瀏覽網頁時,會經常重複使用相同的資源 – 諸如 logo 或 JavaScript 程式碼等,這些資源會在跨頁面間被重複使用。 如果瀏覽器重複地的並下載這些資源,那是非常浪費的。
為了避免這些不必要下載, HTTP 伺服器可以為每個請求指定一個到期時間和一個驗證器,這樣在有效期內可以向瀏覽器指示不需要下載它。 到期時間會告訴瀏覽器能重複使用目前最新響應多長時間,並通過Cache-Control 頭髮送給瀏覽器。驗證器是即便在過期時間以後,也依然允許瀏覽器繼續重新使用網路響應的一種工作方式。 驗證器允許瀏覽器檢查資源是否依然有效並重新使用網路響應。驗證器通過 Last-Modified 或 Etag 標頭髮送。
這裡是一個示例資源:在1小時內到期並具有 last-modified 驗證欄位。
1 2 3 4 5 6 7 |
$ curl https://example.com/foo.png > GET /foo.png < 200 OK < last-modified: Mon, 17 Oct 2016 00:00:00 GMT < cache-control: max-age=3600 <image data> |
該示例中: 接下來的1小時內,接收到這個響應的瀏覽器可以重複使用它,而不用聯絡example.com。 之後,瀏覽器必須通過傳送條件性請求來重新驗證資源,以便檢查影象是否仍然最新:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
$ curl https://example.com/foo.png -H 'if-modified-since: Mon, 17 Oct 2016 00:00:00 GMT' > GET /foo.png > if-modified-since: Mon, 17 Oct 2016 00:00:00 GMT If the image was not modified < 304 Not Modified < last-modified: Mon, 17 Oct 2016 00:00:00 GMT < cache-control: max-age=3600 If the image was modified < 200 OK < last-modified: Tue, 18 Oct 2016 00:00:00 GMT < cache-control: max-age=3600 <image data> |
如果資源未被修改,則傳送未修改(304)網路響應。 相對於重新下載整個資源,這將非常有用,因為需要傳輸的資料將會非常少,但這卻並不能消除瀏覽器與伺服器的通訊延遲。 每次傳送一個未修改的響應時,瀏覽器都已經擁有了正確的資源。 我們想要通過允許客戶端快取更長時間來避免這些浪費的重新驗證。
長期都無需下載的訊號
重新驗證會引發一個困擾:到期時間應該需要多久? 如果你傳送了1小時的到期時間,瀏覽器將不得不與伺服器通訊,來檢查該資源是否每小時修改一次。 但有許多資源,如 Logo 甚至 JavaScript 程式碼很少改變; 在這些情況下,每個小時就過於頻繁了。 另外,如果設定的到期時間過長,瀏覽器則會將快取中過期的資源提供並顯示給使用者。
為解決此問題,Facebook 使用了內容定址URL的概念。 這裡的 URL 不是描述邏輯資源(“logo.png”,“library.js”)的 URL,是我們內容的雜湊值。 每當釋出網站時,我們都會雜湊每個靜態資源。 而且我們會維護一個資料庫,用於儲存這些雜湊值,並將它們對映到各自具體的內容上。 伺服器提供資源時,不按照名稱提供,而是建立一個具有雜湊值的 URL。 例如,如果 logo.png 的雜湊是 abc123 ,則使用 URL http://www.facebook.com/rsrc.php/abc123.png。
因為該方案使用了檔案內容的雜湊作為 URL ,它能保證:內容定址的URL永遠不變。 因此,我們就能提供所有內容定址的 URLs 並設定較長一個的過期時間(目前為 1 年)。 另外,因為這些 URLs 的內容永遠不變,所以伺服器總會對任何及所有條件性的靜態資源請求做出 304 未修改的響應。 這樣不但節省了 CPU 週期,也使伺服器能夠更快地響應這些請求。
重新載入的問題
瀏覽器裡的 reload 按鈕,允許使用者獲取當前頁面的最新內容。 因此,當使用者選擇重新載入時,即使頁面未過期,瀏覽器也會重新驗證當前所在頁面。 而且,還會進一步重新驗證頁面上所有的子資源 – 例如影象和JavaScript檔案。
這種對子資源的重新驗證意味著,即使使用者已經訪問了正在重新載入的站點,每個子資源也都必須經過伺服器讀寫操作。 對使用內容定址 URLs 方式的網站(如 Facebook ),這些重新驗證請求都是徒勞的。 內容定址 URLs 的內容永遠不變,這樣重新驗證的響應結果總是 304 未修改。 換而言之,整個重新載入過程中的重新驗證、請求和資源都不是必要的。
太多條件性請求
在 2014 年,我們發現 60% 的靜態資源網路請求導致了 304 響應.由於內容定址的 URLs 從來不變,這意味著將我們有機會優化 60% 的靜態資源網路請求。 通過使用 Scuba,我們開始探索有關條件性請求的資料。 我們發現不同瀏覽器的效能有實質的差異。
當發現 Chrome 產生最多的是 304 響應時,我們開始與其合作,確認該瀏覽器為何傳送如此多的條件性請求。
Chrome瀏覽器
Chrome 中的一段程式碼為我們尋找答案提供了暗示。 這行程式碼列出了一些原因,包括重新載入、為什麼 Chrome 會要求重新驗證頁面上的資源。比如我們發現, Chrome 會重新驗證通過POST請求載入到網頁上的所有資源。 Chrome團隊告訴我們,原因是 POST 請求的往往是要更改的頁面,例如購物或傳送電子郵件,這些使用者希望擁有最新的頁面。 然而,像Facebook這樣的網站使用POST請求作為使用者登入過程的一部分。 每當使用者登入到 Facebook 時,瀏覽器會忽略其快取並重新驗證以前下載的所有資源。 我們與 Chrome 的產品經理和工程師們合作並確定認 Chrome 的這種請求行為是否必要。 在處理完該問題後,Chrome 所有網路請求中的條件性請求比例從 63% 降低到 24% 。
與 Chrome 在登入問題上的合作,是 Facebook 與瀏覽器廠商共同努力,迅速消除 bug 的很好事例。 通常來說,在查閱資料時,我們經常會在每個瀏覽器級別上分解它們。 如果發現某種瀏覽器異常,表明我們可以優化該瀏覽器中的某些內容。 然後,我們就與該瀏覽器廠商合作解決問題。
實際上,Chrome 條件性請求的百分比依然高於其他瀏覽器,這也似乎預示著 Chrome 還有一些優化空間。 於是我們開始研究 reload,並且發現 Chrome 在處理相同 的 URL 導航時與reload動作行為相同,而其他瀏覽器並非如此。 同一個 URL 導航是指使用者在訪問某個頁面後,試圖通過導航欄再次載入到該同一頁面上的過程。 儘管 Chrome 修復了同一個 URL 動作行為 bug ,但我們並沒有看到明顯的效能變化。 我們開始與 Chrome 小組討論,如何更改 reload 按鈕的動作行為。
更改 reload 按鈕的重新驗證行為是對 Web 長期設計做出的一個變更。 而正如我們討論的這個問題,我們意識到開發者並不太依賴於這種行為。 網站的終端使用者對有效期和有條件性請求一無所知。 雖然有些使用者在想更新最新頁面時可能會按下 reload 按鈕,但 Facebook 的統計資料顯示,大多數使用者不使用 reload 按鈕。 因此,如果開發人員正在更改當前具有X的到期時間的資源,要麼開發人員讓使用者在此之前擁有過期資料,要麼更改 URL 。 如果開發者已經這樣做了,那就沒有理由重新驗證子資源。
關於做什麼,曾有一些辯論,我們也提出了一個妥協的處理方法:永遠不重新驗證那些長期不使用的資源,但對於較短壽命的資源,它們將適用舊的動作行為。 Chrome 團隊考慮到這一點,並決定更改所有快取的資源,而不僅僅是長期存在的資源。您在這裡可以讀到關於它們解決過程的更多詳細內容。 Chrome的一系列措施,使得所有開發人員和網站都可以看到瀏覽器的改進,而並不需要他們自己做任何更改。
該示例中: 與以往不同,當重新載入頁面上的每個子資源都需要一個網路請求時,使用者可以直接從快取中讀取每個檔案,而不會阻塞在網路上。
在Chrome 推出這一最終更改後, Chrome 的條件性請求的百分比急劇下降 – 這對我們的伺服器而言也是一場勝利,它使得伺服器傳送更少的未修改請求,而對使用者而言,他們能更快地重新載入 Facebook 了。
Firefox瀏覽器
在 Chrome 解決了問題後,我們便開始與其他瀏覽器廠商合作解決 reload 按鈕的動作行為問題。 我們向 Firefox 提交了一個 Bug ,而但他們選擇不改變重新載入按鈕的長期動作行為。 而相反的, Firefox 採納了我們一位工程師的建議,為某些資源新增了一個新的快取控制頭,以便告訴瀏覽器無須重新驗證該資源。 這個控制頭背後的想法是,對開發人員及瀏覽器而言,這是一個額外的承諾:這個資源在其最大使用壽命期間永遠不會改變。 Firefox 選擇以用 cache-control: immutable 的頭格式表示此指示。
使用新新增的 headers 後,一個資源到 Facebook 的請求現在將返回如下響應:
1 2 3 4 5 6 7 |
$ curl https://example.com/foo.png > GET /foo.png < 200 OK < last-modified: Mon, 17 Oct 2016 00:00:00 GMT < cache-control: max-age=3600, immutable <image data> |
在 Chrome 完全推出其重新載入按鈕的最終修復版本時,Firefox 也在快速實施快取 cache-control: immutable 更改並將其推出。 你可以在這裡閱讀有關 Firefox 更改的更多資訊。
Firefox 的更改使得開發人員開銷更多,但在我們修改了伺服器以新增不可變 headers 後,我們開始得到非常好的結果。
修復後
Chrome 和 Firefox 的改善措施有效地消除了這些現代版瀏覽器對我們的重新驗證請求 bug 。 不但降低了伺服器的擁堵,更重要的是還改善了使用者訪問 Facebook 的載入時間。
不幸的是,用這種更改方式,我們很難去衡量具體的改善內容- 相比而言,新版本的瀏覽器包含了許太多更改專案,也幾乎不可能將一個特定更改專案所產生的影響單獨隔離開來。 然而,在測試這一更改時,Chrome團隊能夠進行 A / B 測試,該測試發現對於所有網站使用 3G 訪問連線的移動使用者而言,使用更改後瀏覽器中 90% 的載入速度都快了 1.6 秒。
總結
這是一個棘手的課題,因為需要我們修改長期的網路動作行為。 它也突出顯示了Web瀏覽器的特性和功能,以及如何與 Web 開發人員合作,使網路更有利於每個人。 我們很高興在與 Chrome 和 Firefox 團隊協作中建立如此良好的工作關係,並對彼此持續的合作來改善每個人的網路而感到興奮。