Web 項目上線后,開始開門迎客,等待著來自大江南北、有著各式各樣網絡狀態的用戶蒞臨。在千差萬別的網絡狀態中,訪問頁面難免會遇到前端資源加載失敗的情況,占比或許不高,但一遇到,輕則頁面樣式錯亂,重則白屏打不開,影響用戶體驗感受,緊急情況下甚至影響了用戶的工作,屬于非常嚴重的問題。本文將從如何監控加載失敗、加載失敗如何優化、始終加載失敗又該如何處理等問題逐一分析。
如何監控資源加載失敗
方案一:script onerror
我們可以給 script 標簽添加上 onerror 屬性,這樣在加載失敗時觸發事件回調,從而捕捉到異常。
1 |
<script onerror="onError(this)"></script> |
并且,借助構建工具 ( 如 webpack 的 script-ext-html-webpack-plugin 插件) ,我們可以輕易地完成對所有 script 標簽自動化注入 onerror 標簽屬性,不費吹灰之力。
1 2 3 4 5 6 7 |
new ScriptExtHtmlWebpackPlugin({ custom: { test: /\.js$/, attribute: 'onerror', value: 'onError(this)' } }) |
方案二:window.addEventListener
上述方案已然不錯,但我們也試想是否可以減少 onerrror 標簽大量注入呢?類比腳本錯誤 onerror 的全局監控方式(詳見:腳本錯誤量極致優化-監控上報與 Script error),是否也可以通過 window.onerror 去全局監聽加載失敗呢?
答案否定的,因為 onerror 的事件并不會向上冒泡,window.onerror 接收不到加載失敗的錯誤。冒泡雖不行,但捕獲可以!我們可以通過捕獲的方式全局監控加載失敗的錯誤,雖然這也監控到了腳本錯誤,但通過 !(event instanceof ErrorEvent) 判斷便可以篩選出加載失敗的錯誤。
1 2 3 4 5 |
window.addEventListener('error', (event) => { if (!(event instanceof ErrorEvent)) { // todo } }, true); |
通過監控數據分析,我們發現現實情況不容樂觀。訪問頁面時存在資源加載失敗的情況超過了 10000 例/天,且隨著頁面訪問量的上升而增加。
另外,監控資源加載失敗的方式不止這些,上述兩種方式都屬于較好的方案,其他的方式就不再展開。
優化資源加載失敗
方案一:加載失敗時,刷新頁面 (reload)
有了監控數據后,便可著手優化。當資源加載失敗時,刷新頁面可能是最簡單直接的嘗試恢復方式。于是當監控到資源加載失敗時,我們通過 location.reload(true) 強制瀏覽器刷新重新加載資源,并且為了防止出現一直刷新的情況,結合了 SessionStorage 限制自動刷新次數。
通過監控數據發現,通過自動刷新頁面,最終能恢復正常加載占異??偭?30%,優化比例不高,且刷新頁面導致了出現多次的頁面全白,用戶體驗不好。
方案二:針對加載失敗的文件進行重加載
替換域名動態重加載
只對加載失敗的文件進行重加載。并且,為了防止域名劫持等導致加載失敗的原因,對加載失敗文件采用替換域名的方式進行重加載。替換域名的方式可以采用重試多個 cdn 域名,并最終重試到頁面主域名的靜態服務器上(主域名被劫持的可能性?。?/p>
然而,失敗資源重加載成功后,頁面原有的加載順序可能發生變化,最終執行順序發現變化也將導致執行異常。
保證 JS 按順序執行
在不需要考慮兼容性的情況下,資源加載失敗時通過 document.write 寫入新的 script 標簽,可以阻塞后續 script 腳本的執行,直到新標簽加載并執行完畢,從而保證原來的順序。但它在 IE、Edge 卻無法正常工作,滿足不了我們項目的兼容性。
于是我們需要增加 “管理 JS 執行順序” 的邏輯。使 JS 文件加載完成后,先檢查所依賴的文件是否都加載完成,再執行業務邏輯。當存在加載失敗時,則會等待文件加載完成后再執行,從而保證正常執行。
手動管理模塊文件之間的依賴和執行時機存在著較大的維護成本。而實際上現代的模塊打包工具,如 webpack ,已經天然的處理好這個問題。通過分析構建后的代碼可以發現,構建生成的代碼不僅支持模塊間的依賴管理,也支持了上述的等待加載完成后再統一執行的邏輯。
1 2 3 4 5 6 7 8 |
// 檢查是否都加載完成,如是,則開始執行業務邏輯 function checkDeferredModules() { // ... if(fulfilled) { // 所有都加載,開始執行 result = __webpack_require__(__webpack_require__.s = deferredModule[0]); } } |
然而,在默認情況下,業務代碼的執行不會判斷配置的 external 模塊是否存在。所以當 external 文件未加載完成或加載失敗時,使用對應模塊將會導致報錯。
1 2 3 |
"react": (function(module, exports) { eval("(function() { module.exports = window[\"React\"]; }());"); }) |
所以我們需要在業務邏輯執行前,保證所依賴的 external 都加載完成。最終通過開發 wait-external-webpack-plugin webpack 插件,在構建時分析所依賴的 external,并注入監控代碼,等待所有依賴的文件都加載完成后再統一順序執行。(詳見:Webpack 打包后代碼執行時機分析與優化)
至此,針對加載失敗資源重試的邏輯最終都通過構建工具自動完成,對開發者透明。重試后存在加載失敗的情況優化了 99%。減少了大部分原先加載失敗導致異常的情況。
始終加載失敗該怎么辦
用戶網絡千變萬化,或臨時斷網、或瀏覽器突然異常,那些始終加載失敗的情況,我們又該如何應對呢?
一個友好的提醒彈框或是最后的稻草,避免用戶的無效等待,緩解用戶感受。
總結
以上,便是對資源加載失敗優化的整體方案,從如何監控加載失敗、加載失敗時重試、重試失敗后的提醒等方面。大幅優化修正了加載失敗的問題,也緩解著實遇到異常的用戶使用體驗。
如有不妥,懇請斧正,謝謝。