1. 程式人生 > >web資源預載入-生產環境實踐

web資源預載入-生產環境實踐

此文記錄資源預載入在我們專案的實踐,技術難度不算高,重在介紹一套技術方案的誕生與實施,其中都進行了哪些思考,依據什麼來做決策,如何進行效果評估,等等。為讀者在制定技術方案時提供一定啟示。

背景

資源預載入機制很好理解,即在使用者訪問頁面之前,提前載入好相應的資源。這樣使用者在訪問頁面的時候,省去了載入資源的時間,達到“秒開”的效果。

資源預載入的方案很多,本文所述的是純web下的資源預載入,區別於利用容器做資源預置。所以採用的技術都是純web方案。

另外還有一個背景:專案是SPA架構,webpack+vue全家桶,我們做的是在載入完首頁之後的事情,即跳轉其他頁面時的預載入。配合SPA應用的優勢,可以實現媲美原生應用的零延遲跳轉。

下面來介紹下技術細節。

預載入哪些資源

可以預載入的資源是很多的,頁面非同步chunk、vue元件、js模組,甚至是介面資料也可以預請求。所以就要看你的目的是什麼了。

此處我們以零延遲跳轉頁面為目標,所以要預載入的就是頁面非同步chunk。所謂頁面非同步chunk,是指使用vue-router定義的頁面路由所對應的非同步載入的檔案,其實也是個vue元件,為了跟其他vue元件區分,我們管它叫頁面非同步chunk吧。程式碼裡一般這麼寫的:

const routes = [
    name: 'home',
    path: '/home',
    children: [
        {
            path: 'A',
            component: () => import('pageA.vue'),
        },
        {
            path: 'B',
            component: () => import('pageB.vue'),
        }
    ]
]

pageA.vue和pageB.vue打包出的非同步chunk在對應的路由下才會載入,給頁面跳轉帶來延遲感,我們要預載入的就是這部分資源啦。

資源載入時機

明確了要載入的資源,接下來要考慮的是,什麼時候載入這些資源呢?如果我們預載入了pageB而使用者卻不跳轉B怎麼辦呢?

資源載入時機,這是個技術活,需要抓住兩個關鍵點:

1.預載入資源應該儘量不影響使用者的正常操作

2.預載入的資源要儘量保證使用者能使用到

第1點,很容易想到,我們可以在頁面空閒的時候去預載入。而且還要儘早,如果你載入晚了,使用者就用不上你預載入的資源了。總結一下,資源載入時機就是“儘早的在頁面空閒的時候”。

第2點,這就不好辦了,使用者跳不跳B頁面是使用者行為,我們怎麼能保證他一定能用到預載入的資源呢。所以這個只能採取策略,保持一個可接受的比例,在“浪費流量”與“使用者體驗”之間找到權衡點。

下面就這兩點展開說一說。

頁面空閒檢測

如何檢測頁面是否空閒呢?很不幸目前沒有這樣的api可用,有一個requestIdleFrameCallback,支援在每幀繪製空閒期執行一個回撥函式,但不能確保一定執行。

所以我們只能自己想辦法判斷了,思路其實也比較簡單,瀏覽器的渲染程序有js引擎和UI引擎這兩個執行緒,只要這兩個執行緒是空閒的,我們就認為頁面是空閒的,這應該是行得通的。

那麼,如何檢測這兩個執行緒空閒呢?其實很簡單,看程式碼就明白了:

// 檢測js執行緒空閒
const d1 = new Date();
setTimeout(()=>{ 
    const offset = new Date() - d1;
    if (offset < 25 ) { 
      // JS執行緒空閒 
    }
}), 20);

啟動一個延時函式,檢視真正執行時候的延時是多少,如果延時很大,那說明當前js引擎繁忙。如果小於某個閾值,那說明js引擎空閒。

這個閾值該如何確定呢?統計真實使用者的情況肯定是最準確的,所以我們用一個空閒頁面統計了使用者的延時均值,最終確定為5ms。

同樣的思路,UI引擎的空閒也可以檢測出來:

// 檢測UI執行緒空閒
const d1 = new Date();
requestAnimationFrame(()=>{
    const offset = new Date() - d1;
    if ( offset < 30 ) { 
      // UI執行緒空閒 
    }
})

閾值的確定也是統計的真實使用者均值,最終確定為30ms。

有了這兩項檢測,我們就拿到了頁面的“空閒時刻”。那如何“儘早的”拿到呢?做法相對簡單,我們在頁面載入完後用setInterval啟動輪詢,每隔1秒檢測一次,一但檢測到空閒,就進行資源預載入。

資源清單策略

接下來看第2點,如何在“浪費流量”與“使用者體驗”之間找到權衡。

考慮一個問題,如果只有5%的使用者會從首頁跳轉頁面B,那B的資源有必要預載入嗎?答案顯然是不需要。也就是說需要預載入的資源是要人工確定的,那個我們依據什麼來確定資源清單呢?

有兩條途徑:

1.頁面訪問漏斗圖。根據漏斗圖我們能夠得到每一次頁面跳轉的流失率,對於流失率較大的頁面,我們就可以不去預載入了。多大的值算”較大“,這也是需要權衡的,比如我們認為60%就算流失較大了。一個典型的漏斗圖如下:

2.根據統計指標動態調整。我們的資源預載入方案效果怎樣,成本收益比怎樣,是需要明確指標來衡量的。給指標造成負向作用的頁面,就不去預載入它了。

那麼,統計指標該如何設計呢?我們繼續來講。

指標設計

指標是為了用資料化的方式來評估和指導我們的工作、決策。既然是生產環境實踐,就得有一套嚴謹的指標來衡量這個方案的優劣。

上文提到我們統計了使用者執行setTimeout和requestAnimationFrame的延時均值,用於指導我們設定空閒檢測閾值,就是一例應用。

對於整套方案,我們還設計了以下指標:

頁面跳轉時間

即使用者跳轉頁面的耗時均值。這是我們要關注的核心指標,整個方案的目標就是降低頁面跳轉時間。由於是SPA應用,這個時間是比較容易統計到的。

觸發率

即觸發了資源預載入的比例,計算公式:觸發率 = 資源預載入次數 / 頁面載入次數。用來衡量我們的載入策略(空閒檢測)是不是合理,理想情況下這個值應該接近100%,也就是說絕大多數的使用者都預載入到了資源。如果發現觸發率不及預期,那我們應該去調整載入策略。

命中率

即使用者使用了預載入資源的比例,計算公式:使用了預載入的資源次數 / 資源預載入次數。這是用來衡量預載入資源的有效性的,比如一個資源的命中率是80%,說明80%的使用者都使用到了預載入的資源,效果是非常好的。但實際的命中率往往達不到這麼高,所以我們評估的標準是:與漏斗圖的比例越接近,說明效果越好。

頁面停留時間

即使用者在一個頁面的平均停留時間。這個指標也是為了指導載入策略而採集的,比如首頁的平均停留時間是3秒,那麼我們檢查頁面空閒的輪詢間隔可以設為600ms一次,共檢測5次。

除此之外,我們還採集了使用者的網速情況,用以分析這個方案對不同網速段的使用者的影響情況。

有了以上指標,我們就能夠科學的制定策略了,比如某個資源的命中率低於10%,那說明是個低頻頁面,乾脆從清單中剔除掉,不預載入了。

如何載入資源

整個思路已經清晰了起來,接下來到了載入資源環節。話說預載入資源的方式有很多種,我們選哪種呢?

不妨一一來看。

1.手寫script標籤

專案是用webpack打包的,通過manifest檔案可以拿到資源清單,在需要預載入的時候手動建立一個script標籤,使用者真正跳轉的時候就可以使用快取中的資源。

這個方法的優點是侵入性較小,只是多了一次額外的資源載入,對原有程式碼改動很小。

但缺點也很明顯:不好統計指標。要統計載入完成可以在每個script標籤的onload事件裡,還可以接受。但是統計命中率就沒辦法了,沒法判斷使用者使用的是快取中的資源還是新載入的。

2.瀏覽器prefetch

即使用<link rel="prefetch" href="xx.js">,這是瀏覽器提供的預載入資源方式,它會在瀏覽器空閒的時候自動給載入資源。

這個方式能力有點弱,因為沒有js API供呼叫,對我們是黑盒的,沒法進行相關的指標統計。

3.webpack提供的import()

webpack提供的動態載入資源的方法,雖然本質上也是寫script標籤,但webpack給做了很好的封裝,我們通過.then()就可以知道資源載入完畢。而且資源被儲存在記憶體中,一方面便於統計各種指標,另一方面連快取都不必走了,速度更快。

綜上,我們最終選擇了方法3,能夠滿足我們的各項需求。

效果評估

以上就是整個方案的技術內容了,但事情到這裡還不能結束。跟蹤評估方案的效果,並持續優化,這才是生產環境實踐的正確姿勢。

我們關心的核心指標,頁面跳轉時間,有了50%以上的降低。這是預期之內的,之前頁面跳轉比如耗時200ms,命中預載入的資源後,可能一下就到了10ms以內了。這對大盤的影響是很顯著的。

觸發率這個指標,應該至少得在90%以上才算合格。都沒觸發預載入,後續還怎麼談命中。當然這個指標是可以通過調整載入策略來優化的,後面會講。

至於命中率,就不太好說了。首先是上文提過的,它與使用者行為相關,是個不穩定因素。其次,如果資源預載入觸發的太晚,使用者已經自己去跳轉了,也會影響這個指標。所以整體從15%~35%不等吧。這個值是什麼水平呢?合格還是不合格?我們通過翻閱業界也做預載入方案的資料,看到他們得到的命中率也大概在20%,所以可以認為我們的效果也算不錯了。

持續優化

大家也看到了,我們的指標是存在優化空間的,方向就是:提高觸發率、命中率,降低頁面跳轉時間。

上面也有談到,我們為了制定出最精確的載入策略,採集了很多輔助指標。一個有意思的優化是,我們之前是進入頁面1秒後開始檢測空閒的,後來改成了立即進行檢測。沒想到這個改動給觸發率帶來了顯著提升,原因是有部分使用者在1秒內就跳到其他頁面了,根本來不及觸發預載入。

類似的優化還有,我們調整空閒檢測的時間間隔,發現對觸發率也有影響。

另外,我們也根據統計到的命中率,剔除了一些低頻頁面,避免這些資源拉低命中率。

總而言之,各項指標的參考值就是一個權衡的過程,需要根據自己的業務情況來商定。

更多思考

作為一個性能優化的方案,我們只是在特定專案架構、特定場景下做了一部分探索。事實上這個課題能做的還遠不止此。

就比如資源載入策略,難道只能在頁面空閒的時候載入嗎?在PC時代,有些方案是根據滑鼠的軌跡來猜測使用者的下一步動作,進而決定預載入哪些資源。在移動端,我們是否可以根據滑動動作來做預測,又或者,是否可以把使用者行為統計(業務埋點)作為參照,這都是可以探索的方向。

有朋友可能會問,SPA應用把首頁不需要的資源給按需載入,結果你還用預載入又給加回來了,這不是多此一舉嗎?乾脆打包成一個檔案不就行了?聽起來好像很有道理,不過細細思考一下,打包成一個檔案和SPA+預載入,這兩種的優劣各拉出清單對比一下,就知道還是有區別的。這就需要思辨能力了。

所以本文的目的就在於,描述一個技術方案的設計與落地過程,給大家提供技術上或是方法論上的參