網站首頁 編程語言 正文
前言
在一個Hybrid項目中,必不可少的就是加載h5頁面。h5頁面的加載性能極大影響著用戶體驗,并會從各方面影響到我們APP的業務數據。試想,假設一個h5頁面要花好幾秒才能打開,那用戶還會使用我們的APP嗎?所以今天我們講一講,客戶端上優化h5頁面加載速度的一種方式:預渲染。
照例,拋出本篇文章要解決的幾個問題:
- 客戶端可以從哪些方面優化h5頁面的加載速度?
- 預渲染的基本實現邏輯是怎樣的?
- 預渲染存在哪些局限性?
術語對齊
術語 | 描述 |
---|---|
WebView | 用于承載h5頁面的native組件。 |
預創建 | 在用戶打開一個h5頁面之前,在內存中先創建好一個WebView實例。當用戶打開h5頁面時,可以直接取到預先創建好的WebView實例,用于承載h5頁面。 |
預渲染 | 在用戶打開一個h5頁面之前,在內存中不僅預創建好了WebView實例,還進一步根據url提前渲染好了WebView。當用戶打開指定的url頁面時,可以直接拿預先渲染好了的WebView進行展示。 |
客戶端可以從哪些方面優化h5頁面的加載速度?
我們可以看一下,在Android上完整打開一個WebView需要經歷怎樣的一個鏈路(來自優秀的前輩們):
所以,要想優化WebView的加載速度,就要想辦法去縮短鏈路中這些節點所耗費的時間。
從客戶端的角度上來講,Page初始化就是H5容器的初始化。一般而言,容器初始化時間是比較快的(如果沒有太多初始化邏輯的話),優化空間有限。WebView的初始化相對就比較復雜,涉及到了瀏覽器內核在主線程的初始化。APP冷啟動后,首次創建WebView時需要去初始化瀏覽器內核。
這里要分3種情況去看:
- 全新安裝APP,冷啟動后首次打開一個WebView,耗時最長,可能會需要1000ms左右,取決于瀏覽器內核。
- 非全新安裝APP,冷啟動后首次打開一個WebView,耗時分布在500ms左右。
- 冷啟動后,非首次打開一個WebView,耗時就非常短了,分布在15ms左右。
這里我們可以看到,第1種和第2種情況是存在比較大的提升空間的。
當我們創建好WebView,執行WebView#loadUrl()時,WebView就會經歷上圖中的白屏-loading-可交互狀態。這幾個階段,可以說是整個鏈路中耗時占比最大的一部分。但客戶端在這里能做的優化是很有限的,而且通常需要跟前端、服務端去配合優化,比如可以并行請求數據,這里就先不發散了。但如果換個角度思考,客戶端先不去干涉后面這幾個階段的邏輯,而是提前去執行后面這幾個階段的邏輯,那么不也就相當于提高加載速度了么?這其實就是我們要講的預加載。
優化思路
所以我們的優化思路主要針對WebView初始化階段,以及WebView加載階段。
通過預創建WebView,去解決首次創建WebView耗時長的問題。
通過預渲染WebView,去提前經歷用戶需要等待的白屏-loading階段,當用戶打開相應頁面時,能夠直接上屏展示,給用戶的感覺就是秒開。
預渲染的基本實現邏輯是怎樣的?
預創建
預創建是預渲染的前提(沒有預創建好怎么預渲染呢..),所以我們先講下預創建。預創建WebView,一個基本原則就是,當內存中沒有預創建的WebView可以復用(即預創建沒有命中)時,就走原來創建WebView的邏輯。
預創建個數
這里我們選擇只預創建1個WebView。之所以選擇1個,是因為我們預創建WebView的根本目的,是為了解決APP首次安裝/冷啟動時,第一個WebView加載慢的問題。后續的WebView實例的創建都是很快的。所以,即使后面沒有命中預創建的WebView,用的重新創建的WebView,也就是多花了15ms左右的時間,影響是很小的。所以綜合下來,預創建1個WebView的性價比是最高的,多了反而浪費內存。
預創建時機
這里的時機要分為三個,第一個時機是在冷啟動后,我們需要進行預創建。可以選擇把這個時機放到進入首頁后,用IdleHandler進行主線程閑時創建。當然也可以選擇前置。前置的話有可能會影響到APP的啟動,所以如果不是特別有必要的話,建議還是后置一些。
第二個時機是在預創建的WebView被拿去復用后,此時也是需要預創建的。因為一旦被拿去復用,意味著我們緩存中已經沒有可用的WebView了,若一個pha頁面又打開了另外一個pha頁面,我們在這個case最好也能提供預創建的WebView。
以上兩個時機都是自動觸發。后來發現一個場景,當用戶在某個路徑比較深的頁面時,若需要預加載下一個頁面,那么這個頁面往往是不需要一冷啟動就預渲染的。這時候就需要一個接口讓業務方能在用戶打開頁面之前將該頁面進行預加載。
void preload(Context context, String url);
預創建復用
復用WebView需要注意一點,每個WebView都是跟指定的Context綁定的,但預創建時,還獲取不到WebView未來要綁定的Context。因此預創建時可以用MutableContextWrapper包ApplicationContext去創建。MutableContextWrapper支持我們將其中的BaseContext進行替換,復用預創建的WebView時,將ApplicationContext替換為需要綁定的Context即可。同時根據“預創建時機”中說的,在復用時,往棧頂插入一個新的預創建的WebView。相應的,當頁面關閉時,我們也需要將綁定的Context解綁,防止內存泄漏。
大致的邏輯如下圖所示:
這里也貼出部分偽代碼:
/** * 閑時預創建 */ private void preCreateWebView() { Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() { @Override public boolean queueIdle() { // 預創建webView WebView webView = new WebView(new MutableContextWrapper(APPLICATION_CONTEXT)); cache.add(webView); return false; } }); } /** * 獲取預創建的WebView * @param context 要與使用的webview綁定的context * @return */ public PHAWebView acquirePreCreateWebView(Context context) { WebView webview; // 緩存中無可用WebView時,直接新建 if (cache.isEmpty()) { webview = createWebView(); } else { webview = cache.peekWebView(); } // 更改context if (webview != null && webview.getContext() instanceof MutableContextWrapper) { MutableContextWrapper webViewMutableContext = (MutableContextWrapper) webview.getContext(); webViewMutableContext.setBaseContext(context); } return webview; }
預渲染
預渲染,其實就是在預創建的基礎上,執行WebView#loadUrl(),將頁面提前渲染完成。但這里面還有一些細節點需要關注。
預渲染時機
預渲染的時機也是分為兩個,一個是冷啟動后的主線程閑時階段進行預渲染,這一點跟預加載的時機保持一致。還有一個我們可以選擇在頁面關閉時進行預渲染。打比方說,假設我預渲染了頁面A,那么用戶在訪問完頁面A后,我需要再次預渲染頁面A,從而保證頁面A的實效性。
預渲染白名單
首先,預渲染是有對象的,預渲染的對象就是頁面url。而預創建是沒有特定對象的,只需要隨時準備一個可用的WebView就行了,誰都可以用。但預渲染不行,不告訴我預渲染誰,我還怎么預渲染。
所以,從初始化開始,我們就已經決定好了該預渲染哪些頁面,也就是預渲染白名單。白名單可通過服務器配置的方式進行下發。但也不能一股腦把頁面全都配上,因為內存是有限的,配太多可能會引起低端機型的OOM。
預渲染有效性校驗
所謂的有效性校驗,就是在復用預渲染WebView時,校驗這個WebView是否被正常預渲染了。如果失效,就走預創建/重新創建的邏輯。這里分兩個角度來校驗WebView的有效性:時間有效性與狀態有效性。
時間有效性
存在這樣一種場景:當我們已經預渲染了A頁面,且用戶一直沒有訪問。某個時刻這個頁面做了更新,即重新發布了,那么如果用戶這時候去訪問A頁面,看到的還是舊的A頁面。所以這里需要在預渲染頁面時,給頁面設置一個過期時間,若復用預渲染WebView時已經過期了,就說明WebView已經失效了,需要重新loadUrl保證頁面的實效性。
狀態有效性
狀態有效性就是去校驗預渲染的WebView最終是否有渲染成功,這一點我們可以通過WebViewClient的生命周期回調(onPageFinished/onReceivedError)來進行判斷。之所以要做這個校驗,是為了防止一開始預渲染就失敗了,卻還是拿這個WebView去進行展示。比如,假設我們在網絡異常狀態下去進行了預渲染,在網絡恢復正常后用戶訪問預渲染頁面,若不進行狀態校驗,那么看到的就會是網絡異常狀態下的WebView了。
頁面顯示狀態通知
頁面顯示狀態,通俗來講就是頁面現在是離屏的,還是上屏的。在h5的一些業務場景中,有一部分是需要感知到頁面的顯示狀態的。比如引導類的動畫,比如會場的一些倒計時等等。所以我們需要將頁面的顯示狀態同步到h5那邊。
實現上,就是要在預渲染WebView時給h5注入一個全局的環境變量,window.page_on_screen=false
。當復用WebView,即上屏時,再將window.page_on_screen
設置為true,同時發一個通知給h5。這樣h5就可以根據同步到的顯示狀態來控制自己的業務邏輯。
其它注意事項
預渲染、預創建,本質上是用空間換時間的優化,所以是比較耗費內存的。所以我們需要在內存不足的時候,及時將內存中待使用的WebView給回收掉,避免APP發生OOM。
另外,因為預渲染離屏加載了頁面,所以頁面的初始化行為是需要納入評估的,只有評估通過后,才能放入預渲染白名單中。具體的初始化行為包括但不限于:業務的曝光埋點、前端邏輯(如倒計時、跨天活動)、消費型(如首次引導)、后端流量評估、頁面在后臺是否會有聲音、是否會彈框(系統框、權限框、對話框...)等等。
預渲染存在哪些局限性?
- 低端機內存空間有限,預渲染白名單的實際配置數量需要視情況進行調整。
- 預渲染頁面必須經過白名單配置。頁面url、參數發生改變,配置也需要改變。這一點其實也是有優化空間的,即離屏預渲染時加載url前綴域名,上屏時再根據完整的url參數做邏輯調整。實現上會比較麻煩,可以視ROI情況進行投入。
- 預渲染頁面的實效性無法保證。預渲染頁面一旦重新部署,端上是不能立刻感知到并重新加載的。按上面的預渲染時機,目前只有以下二個場景會觸發端上對預渲染頁面的更新:
- 1.冷啟動;
- 2.頁面被訪問后關閉;
- 3.業務調用接口主動注入。
所以大家如果有比較好的方案歡迎分享給我呀!
- 命中率。預渲染頁面是不能百分百命中的,即即使我們把某個頁面配置進了預渲染白名單,app也有可能沒預渲染上這個頁面。有很多異常場景會影響到命中率,比如:
- 1.上面講到的預渲染的時間有效性與狀態有效性;
- 2.服務端下發的預渲染白名單沒有及時拉取到;
- 3.主線程一直繁忙,導致預渲染邏輯一直沒執行;
- 4.內存不夠,將緩存的預渲染WebView回收掉了。
總結
所以雖然預渲染能從表面上實現h5頁面的秒開,但也不是萬能的,是存在一些缺陷的(否則也不需要別的優化手段了)。但我認為是諸多優化手段中比較簡單卻又能立竿見影的一個手段,特別是對本身h5頁面加載就非常慢的app而言。所以如果還沒做起來的同學可以試一試,后面再結合其它優化的手段抹除不足。今天就講到這里啦。
原文鏈接:https://juejin.cn/post/7120907970031910942
相關推薦
- 2024-03-04 新版ECharts實現“暫無數據”的完美解決方案
- 2022-11-27 Python?OpenCV實現圖像增強操作詳解_python
- 2022-10-28 Winform?控件優化LayeredWindow無鋸齒圓角窗體_C#教程
- 2022-10-24 vscode使用Eslint+Prettier格式化代碼的詳細操作_C 語言
- 2022-07-04 一文搞懂???????python可迭代對象,迭代器,生成器,協程_python
- 2022-05-11 linq中的分組操作符_實用技巧
- 2022-12-06 R語言如何畫豎線、橫線、添加標簽以及畫固定長度的線段_R語言
- 2022-12-04 react?component?function組件使用詳解_React
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支