瀏覽器往返緩存(Back/Forward cache)問題的分析與解決

博客源地址: https://github.com/LeuisKen/l...
相關討論還請到源 issue 下。

什麼是往返緩存(Back/Forward cache)

往返緩存(Back/Forward cache,下文中簡稱bfcache)是瀏覽器爲了在用戶頁面間執行前進後退操做時擁有更加流暢體驗的一種策略。該策略具體表現爲,當用戶前往新頁面時,將當前頁面的瀏覽器DOM狀態保存到bfcache中;當用戶點擊後退按鈕的時候,將頁面直接從bfcache中加載,節省了網絡請求的時間。javascript

可是bfcache的引入,致使了不少問題。下面,舉一個咱們遇到的場景:html

sample

頁面A是一個任務列表,用戶從A頁面選擇了「任務1:看新聞」,點擊「去完成」跳轉到B頁面。當用戶進入B頁面後,任務完成。此時用戶點擊回退按鈕,會回退到A頁面。此時的A頁面「任務1:看新聞」的按鈕,應該須要標記爲「已完成」,因爲bfcache的存在,當存入bfcache時,「任務1」的按鈕是「去完成」,因此此時回來,按鈕也是「去完成」,而不會標記爲「已完成」。java

既然bug產生了,咱們該如何去解決它?不少文章都會提到unload事件,可是咱們實際進行了測試發現並很差用。因而,爲了解決問題,咱們的bfcache探祕之旅開始了。git

bfcache 探祕

在檢索page cache in chromium的時候,咱們發現了這個issue:https://bugs.chromium.org/p/c... 。裏面提到 chromium(chrome的開源版本)在好久之前就已經將PageCache(即bfcache)這部分代碼移除了。也就是說如今的chrome應該是沒有這個東西的。能夠肯定的是,chrome之前的版本中,bfcache的實現是從webkit中拿來的,加上咱們項目目前面向的用戶主體就是 iOS + Android,iOS下是基於Webkit,Android基於chrome(且這部分功能也是源於webkit)。所以追溯這個問題,咱們只要專一於研究webkitbfcache的邏輯便可。github

一樣經過上文中描述的commit記錄,咱們也很快定位到了PageCache相關邏輯在Webkit中的位置:webkit/Source/WebCore/history/PageCache.cppweb

該文件中包含的兩個方法引發了咱們的注意:canCachePagecanCacheFrame。這裏的Page便是咱們一般理解中的「網頁」,而咱們也知道網頁中能夠嵌套<frame><iframe>等標籤來置入其餘頁面。因此,PageFrame的概念就很明確了。而在canCachePage方法中,是調用了canCacheFrame的,以下:chrome

// 給定 page 的 mainFrame 被傳入了 canCacheFrame
bool isCacheable = canCacheFrame(page.mainFrame(), diagnosticLoggingClient, indentLevel + 1);

源代碼連接:webkit/Source/WebCore/history/PageCache.cpp瀏覽器

所以,重頭戲就在canCacheFrame了。緩存

canCacheFrame方法返回的是一個布爾值,也就是其中變量isCacheable的值。那麼,isCacheable的判斷策略是什麼?更重要的,這裏面的策略,有哪些是咱們可以利用到的。網絡

注意到這裏的代碼:

Vector<ActiveDOMObject*> unsuspendableObjects;
if (frame.document() && !frame.document()->canSuspendActiveDOMObjectsForDocumentSuspension(&unsuspendableObjects)) {
    // do something...
    isCacheable = false;
}

源代碼連接:webkit/Source/WebCore/history/PageCache.cpp

很明顯canSuspendActiveDOMObjectsForDocumentSuspension是一個很是重要的方法,該方法中的重要信息見以下代碼:

bool ScriptExecutionContext::canSuspendActiveDOMObjectsForDocumentSuspension(Vector<ActiveDOMObject*>* unsuspendableObjects)
{

    // something here...

    bool canSuspend = true;

    // something here...

    // We assume that m_activeDOMObjects will not change during iteration: canSuspend
    // functions should not add new active DOM objects, nor execute arbitrary JavaScript.
    // An ASSERT_WITH_SECURITY_IMPLICATION or RELEASE_ASSERT will fire if this happens, but it's important to code
    // canSuspend functions so it will not happen!
    ScriptDisallowedScope::InMainThread scriptDisallowedScope;
    for (auto* activeDOMObject : m_activeDOMObjects) {
        if (!activeDOMObject->canSuspendForDocumentSuspension()) {
            canSuspend = false;
            // someting here
        }
    }

    // something here...

    return canSuspend;
}

源代碼連接:webkit/Source/WebCore/dom/ScriptExecutionContext.cpp

在這一部分,能夠看到他調用每個 ActiveDOMObjectcanSuspendForDocumentSuspension 方法,只要有一個返回了falsecanSuspend就會是false(Suspend這個單詞是掛起的意思,也就是說存入bfcache對於瀏覽器來講就是把頁面上的frame掛起了)。

接下來,關鍵的ActiveDOMObject定義在:webkit/Source/WebCore/dom/ActiveDOMObject.h ,該文件這部分註釋,已經告訴了咱們最想要的信息。

The canSuspendForDocumentSuspension() function is used by the caller if there is a choice between suspending and stopping. For example, a page won't be suspended and placed in the back/forward cache if it contains any objects that cannot be suspended.

canSuspendForDocumentSuspension 用於幫助函數調用者在「掛起(suspending)」與「中止」間作出選擇。例如,一個頁面若是包含任何不能被掛起的對象的話,那麼它就不會被掛起並放到PageCache中。

接下來,咱們要找的就是,哪些對象是不能被掛起的?在WebCore目錄下,搜索包含canSuspendForDocumentSuspension() const關鍵字的.cpp文件,能找到48個結果。大概看了一下,最好用的objects that cannot be suspended應該就是Worker對象了,見代碼:

bool Worker::canSuspendForDocumentSuspension() const
{
    // 這裏實際上是有一個 FIXME 的,看來 webkit 團隊也以爲直接 return false 有點簡單粗暴。
    // 不過仍是等哪天他們真的修了再說吧
    // FIXME: It is not currently possible to suspend a worker, so pages with workers can not go into page cache.
    return false;
}

源代碼連接:webkit/Source/WebCore/workers/Worker.cpp

解決方案

業務上添加以下代碼:

// disable bfcache
try {
    var bfWorker = new Worker(window.URL.createObjectURL(new Blob(['1'])));
    window.addEventListener('unload', function () {
        // 這裏綁個事件,構造一個閉包,以避免 worker 被垃圾回收致使邏輯失效
        bfWorker.terminate();
    });
}
catch (e) {
    // if you want to do something here.
}

Thanks to

相關連接

相關文章
相關標籤/搜索