點擊瀏覽器的前進后退按鈕時,頁面的緩存機制
這是一篇譯文
原文標題:Back/forward cache
原文鏈接:https://web.dev/bfcache/
后退/前進緩存(Back/forward cache, 以下簡稱bfcache)是一種瀏覽器優化,可實現即時的后退和前進導航。它顯著改善了用戶的瀏覽體驗,尤其是那些網絡或設備速度較慢的用戶。
作為web開發人員,了解如何在所有瀏覽器上基于bfcache優化頁面非常重要。這樣可以提高用戶體驗。
瀏覽器兼容性
Firefox和Safari都早已支持bfcache,包括桌面和移動設備。
從86版開始,Chrome已經為一小部分用戶啟用了Android上的跨站點導航。在chrome87中,bfcache支持將推廣到所有Android用戶進行跨站點導航,目的是在不久的將來也支持相同的站點導航。
bfcache基礎知識
bfcache是一個內存中的緩存,它在用戶離開時存儲頁面的完整快照(包括JavaScript堆)。由于整個頁面都在內存中,如果用戶決定返回,瀏覽器可以快速輕松地恢復頁面。
有多少次你訪問一個網站,點擊一個鏈接進入另一個頁面,卻發現這不是你想要的,然后點擊后退按鈕?此時,bfcache對上一頁的加載速度會有很大的影響:
不支持bfc時:將啟動一個新的請求來加載上一個頁面,并且,根據該頁面針對重復訪問的優化程度,瀏覽器可能需要重新下載、重新解析和重新執行剛下載的部分(或全部)資源。
開啟了bfc時:加載上一個頁面基本上是即時的,因為整個頁面可以從內存中恢復,而不必訪問網絡。
bfcache不僅加快了導航速度,還減少了數據使用,因為不必再次下載資源。
Chrome的使用數據顯示,桌面上十分之一的導航和手機上五分之一的導航要么后退要么前進。啟用bfcache后,瀏覽器可以消除每天數十億個網頁的數據傳輸和加載時間!
cache是如何工作的
bfcache使用的“緩存”不同于HTTP緩存(這在加速重復導航方面也很有用)。bfcache是內存中整個頁面的快照(包括JavaScript堆),而HTTP緩存只包含以前發出的請求的響應。由于加載頁面所需的所有請求都能從HTTP緩存中得到滿足的情況非常罕見,因此使用bfcache恢復進行的重復訪問總是比最優化的非bfcache導航更快。
然而,在內存中創建頁面快照是有一定復雜性的,特別是涉及到如何最好地保存正在進行的代碼。例如,當頁面在bfcache中時,如何處理到達超時的setTimeout()調用?
答案是,瀏覽器暫停運行任何掛起的計時器或未resolved的Promise(實際上是JavaScript任務隊列中所有掛起的任務),并在頁面從bfcache恢復時(或如果)恢復處理任務。
在某些情況下,暫停任務是低風險的(例如,超時或Promise),但在其他情況下,它可能會導致非常混亂或意外的行為。例如,如果瀏覽器暫停IndexedDB事務中所需的任務,它可能會影響同一源中打開的其他選項卡(因為多個選項卡可以同時訪問同一個IndexedDB數據庫)。因此,瀏覽器通常不會嘗試在IndexedDB事務中間緩存頁面,也不會使用可能影響其他頁面的api。
有關各種API用法如何影響頁面的bfcache的詳細信息,請參考下文的內容。
監聽bfcache的API
雖然bfcache是瀏覽器自動進行的一種優化,但對于開發人員來說,知道何時發生這種情況仍然很重要,這樣他們就可以針對bfcache優化自己的頁面,并相應地調整任何指標或性能度量。
用于觀察bfcache的主要事件是頁面轉換事件pageshow和pagehide,這兩個事件存在的時間和bfcache存在的時間一樣長,并且在當今使用的幾乎所有瀏覽器中都受支持。
新的頁面生命周期事件 freeze 和 resume 也會在頁面進入或離開bfcache時以及在其他一些情況下觸發。例如,當后臺選項卡凍結以最小化CPU使用率時。注意,頁面生命周期事件目前僅在基于Chromium的瀏覽器中受支持。
監聽頁面從bfc中恢復
當頁面最初加載時,pageshow事件在load事件之后立即激發。另外,頁面從bfcache還原時,pageshow也會觸發。pageshow事件有一個persisted屬性,如果從bfcache還原頁面,則該屬性為true;如果不是,則為false。您可以使用persisted屬性來區分常規頁面加載和bfcache還原。例如:
- window.addEventListener('pageshow', function(event) {
- if (event.persisted === true) {
- // 頁面從bfc中恢復
- console.log('This page was restored from the bfcache.');
- } else {
- // 頁面正常加載
- console.log('This page was loaded normally.');
- }
- });
在支持頁面生命周期API的瀏覽器中,當頁面從bfcache還原時(就在pageshow事件之前),resume事件也會觸發,不過當用戶重新訪問凍結的背景選項卡時,它也會觸發。如果要在凍結頁面(包括bfcache中的頁面)后恢復頁面狀態,可以使用freeze事件,但如果要測量站點的bfcache命中率,則需要使用pageshow事件。在某些情況下,您可能需要同時使用這兩種方法。
監聽頁面進入bfc
pagehide事件是pageshow事件的對應項。當頁面正常加載或從bfcache還原時,將激發pageshow事件。pagehide事件在頁面正常卸載或瀏覽器試圖將其放入bfcache時觸發。
pagehide事件還有一個persistent屬性,如果它是false,那么您可以確信頁面不會進入bfcache。但是,如果persistent屬性為true,則不能保證將緩存頁。這意味著瀏覽器打算緩存頁面,但可能有一些因素導致無法緩存。
- window.addEventListener('pagehide', function(event) {
- if (event.persisted === true) {
- // 頁面可能會進入bfc緩存
- console.log('This page *might* be entering the bfcache.');
- } else {
- // 頁面會正常退出,并且會被丟棄
- console.log('This page will unload normally and be discarded.');
- }
- });
類似地,freeze事件將在pagehide事件之后立即觸發(如果事件的persistent屬性為true),但這同樣意味著瀏覽器打算緩存頁面。在下面描述的情況下,它可能仍然必須丟棄它。
為bfcache優化頁面
并不是所有的頁面都存儲在bfcache中,即使頁面確實存儲在那里,它也不會無限期地停留在那里。開發人員必須了解是什么使頁面符合bfcache的條件(和不符合條件),以最大限度地提高緩存命中率。
下面幾節概括了使瀏覽器盡可能緩存頁面的最佳實踐。
不要使用 unload 事件
在所有瀏覽器中優化bfcache的最重要方法是永遠不要使用unload事件。
unload事件對于瀏覽器來說是有問題的,因為它早于bfcache觸發,并且網絡上的許多頁面都是在一個(合理的)假設下運行的:unload事件觸發后,頁面將不再存在了。這就帶來了一個挑戰,因為許多頁面的構建都是基于這樣一個假設:unload事件將在用戶離開時觸發。然而,事實已經不是這樣了(而且在很長一段時間內都不是這樣)。
譯者注:這里我的理解是,瀏覽器在設計unload事件之初,就是在頁面不需要的時候觸發。如果開發者監聽了unload事件,則表示頁面銷毀時需要執行一些邏輯,這個時候,頁面自然是不需要再進行緩存了。然而這種情況下,很多開發者是希望頁面被緩存的,這和unload事件本身的含義有沖突。
所以瀏覽器面臨著一個兩難的選擇,他們必須在能改善用戶體驗的同時也可能有破壞頁面的風險。
Firefox選擇了如果添加unload偵聽器,那么頁面就不符合bfcache的條件,這樣做風險較小,但也會使很多頁面無法bfc。Safari會嘗試緩存一些監聽了unload事件的頁面,但是為了減少潛在的破壞,當用戶導航離開時,Safari不會觸發unload事件。
由于Chrome中65%的頁面都注冊了unload事件偵聽器,為了能夠緩存盡可能多的頁面,Chrome選擇與Safari保持一致。
不要使用unload事件,使用pagehide事件。pagehide事件在unload事件觸發的所有情況下都會觸發,并且在頁面放入bfcache時也會觸發。
注意,永遠不要添加unload事件偵聽器!請改用pagehide事件。在Firefox中添加unload事件監聽器會使你的站點變慢,而代碼在Chrome和Safari中大部分時間都不會運行。
僅僅有條件的添加 beforeunload 事件
beforeunload事件不會使您的頁面不符合Chrome或Safari的bfcache,但在Firefox中不行,因此除非絕對必要,否則請避免使用它。
但是,與unload事件不同,beforeunload有合法的用法。例如,當您要警告用戶他們有未保存的更改時,如果他們離開頁面,他們將丟失。在這種情況下,建議僅在用戶有未保存的更改時添加beforeunload偵聽器,然后在保存未保存的更改后立即將其刪除。
下面的寫法是?的(無條件的監聽了beforeunload事件):
- window.addEventListener('beforeunload', (event) => {
- if (pageHasUnsavedChanges()) {
- event.preventDefault();
- return event.returnValue = 'Are you sure you want to exit?';
- }
- });
下面的寫法是?的:
- function beforeUnloadListener(event) {
- event.preventDefault();
- return event.returnValue = 'Are you sure you want to exit?';
- };
- // A function that invokes a callback when the page has unsaved changes.
- // 當頁面內容未保存時,才監聽beforeunload事件
- onPageHasUnsavedChanges(() => {
- window.addEventListener('beforeunload', beforeUnloadListener);
- });
- // A function that invokes a callback when the page's unsaved changes are resolved.
- // 當頁面內容保存完畢時,移除beforeunload事件
- onAllChangesSaved(() => {
- window.removeEventListener('beforeunload', beforeUnloadListener);
- });
避免window.opener的references
在一些瀏覽器中(包括chrome,從86版本起),如果使用 window.open或者 target=_blank 打開一個新的頁面,但是沒有寫明:rel="noopener",那么,新打開的頁面中,會包含一個對原有頁面的引用。
除了存在安全風險外,保留了對其他頁面引用的頁面不能安全地放入bfcache,因為這可能會破壞任何試圖訪問它的頁面。
譯者注:安全問題可以參考本公眾號的這篇文章 :



























