Webview載入H5優化小記
行到水窮處,坐看雲起時
原文連結
一、概述
1、背景
鑑於H5的優勢,客戶端的很多業務都由H5來實現,Webview成了App中H5業務的唯一載體。WebView元件是iOS元件體系中非常重要的一個,之前的UIWebView 存在嚴重的效能和記憶體消耗問題,iOS 8之後推出WKWebView,旨在代替UIWebView;WKWebView在效能、穩定性、記憶體佔用上有很大的提升,支援更多的HTML5特性,高達60fps的滾動重新整理率以及內建手勢;可以通過KVO監控網路載入的進度,獲取網頁title;實踐中,大部分App的H5業務將由WKWebview承載。2、H5頁面的體驗問題
從使用者角度,相比Native頁面,H5頁面的體驗問題主要有兩點:
頁面開啟時間慢:開啟一個 H5 頁面需要做一系列處理,會有一段白屏時間,體驗糟糕。響應流暢度較差:由於 WebKit 的渲染機制,單執行緒,歷史包袱等原因,頁面重新整理/互動的效能體驗不如原生。這裡討論的是:第一點,怎樣減少白屏時間。
二、Webview開啟H5
通過Webview開啟H5頁面,請求並得到 HTML、CSS 和 JavaScript 等資源並對其進行處理從而渲染出 Web 頁面。
1、載入流程
DOM渲染之前耗時主要在兩部分:初始化Webview 和 資料請求,一般Webview首次初始化在400ms這個量級,二次載入能少一個量級。資料請求依賴網路,網路請求一般經過:DNS查詢、TCP 連線、HTTP 請求和響應。資料包括HTML、JS和CSS資源,這些都是在webview在loadRequest:之後做的,這一階段,使用者所見到的都是白屏。(雖然4G已經成為主流,但是4G延遲明顯高於Wifi)。2、H5頁面渲染
對H5頁面的渲染,主要包括:渲染樹構建、佈局及繪製,具體可分為:
處理 HTML 標記並構建 DOM 樹。處理 CSS 標記並構建 CSSOM(CSS Object Model) 樹。將 DOM 與 CSSOM 合併成一個渲染樹。根據渲染樹來佈局,以計算每個節點的幾何資訊。將各個節點繪製到螢幕上。說明:這五個步驟並不一定一次性順序完成。如果 DOM 或 CSSOM 被修改,以上過程需要重複執行,這樣才能計算出哪些畫素需要在螢幕上進行重新渲染。實際頁面中,CSS 與 JavaScript 往往會多次修改 DOM 和 CSSOM。具體參考:DOM渲染機制與常見效能優化
3、總結
分析Webview開啟H5開啟的過程,我們發現,在H5優化中,前端重任在肩;降低請求量:合併資源,減少 HTTP 請求數,minify / gzip 壓縮,webP,lazyLoad。加快請求速度:預解析DNS,減少域名數,並行載入,CDN 分發。快取:HTTP 協議快取請求,離線快取 manifest,離線資料快取localStorage。渲染:JS/CSS優化,載入順序,服務端渲染,pipeline。複製程式碼但是客戶端也很重要,主要優化DOM渲染之前這些事情,可以做有:減少DNS時間、預初始化WebView 以及 HTML、JS、CSS等資源離線下載。列舉在某業務中筆者實踐過的比較trick的優化方案,然後再引出筆者認為理想的方案。
二、WebView的客戶端優化(trick版)
由於是接入第三方的H5頁面,接入離線包方案,需要比較繁雜的商務溝通和技術挑戰(業務邏輯和程式碼超級詭異),臨時採用如下優化方案。
1、預載入資源
將首頁面需要的JS檔案和CSS檔案等資源放在一個URL地址(和業務url同域名);啟動App後,間隔X秒去載入;載入的策略是,檢查當前和上一次間隔時間,超時則載入,有效期忽略預載入請求。2、預初始化Webview
首次初始化Webview,需要初始化瀏覽器核心,需要的時間在400ms這個量級;二次初始化時間在幾十ms這個量級;根據此特徵:選擇在APP 啟動後X秒,預建立(初始化)一個 Webview 然後釋放,這樣等使用到 H5 模組,再載入 Webview時,載入時間也少了不少。結合步驟一中預載入公共資源,也需要Webview,所以選擇在載入公共資源包時候,首次初始化Webview,載入資源,然後釋放。3、最終方案(迫不得已)
由於第三方業務H5很多問題,和人力上不足;不得不需要客戶端強行配合優化,在產品的要求下,不得不採用如下方案,方案的前提是:業務H5儘可能少修改,甚至不修改,客戶端還要保證首屏載入快;
預載入資源預建立Webview並載入首頁H5,駐留在記憶體中,需要的時候,立刻顯示。4、方案的後遺症
我不建議這種trick做法,因為自從開了這個口子,後續很多H5需求不走之前既定的離線包方案,在記憶體中預建立多個Webview (最多4個),載入H5時候不用新建Webview,從Webview池中獲取;此種Webview池方案帶來諸多隱患:記憶體壓力、詭異的白屏、JS造成的記憶體洩露,頁面的清空等等問題(填坑填到掉頭髮)。三、離線包方案
1、概述
離線包方案才是業務主流的H5載入優化方案,非常建議在客戶端團隊和前端團隊推廣,類似預建立Webview載入H5不應該成為主流。將每個獨立的H5功能模組,相關HTML、Javascript、CSS 等頁面內靜態資源打包到一個壓縮包內,客戶端可以下載該離線包到本地,然後開啟Webview,直接從本地載入離線包,從而最大程度地擺脫網路環境對 H5 頁面的影響。離線包可以提升使用者體驗(頁面載入更快),還可以實現動態更新(在推出新版本或是緊急釋出的時候,可以把修改的資源放入離線包,通過更新配置讓應用自動下載更新)2、方案描述
引用bang的離線包方案,簡單描述如下:
後端使用構建工具把同一個業務模組相關的頁面和資源打包成一個檔案,同時對檔案加密/簽名。客戶端根據配置表,在自定義時機去把離線包拉下來,做解壓/解密/校驗等工作。根據配置表,開啟某個業務時轉接到開啟離線包的入口頁面。攔截網路請求,對於離線包已經有的檔案,直接讀取離線包資料返回,否則走 HTTP 協議快取邏輯。離線包更新時,根據版本號後臺下發兩個版本間的 diff 資料,客戶端合併,增量更新。說明:目前WKWebView已經能成為主流,但是WKWebView在實現離線包方案時,攔截網路請求有坑。
3、WKWebView攔截網路請求的坑
雖然NSURLProtocol可以攔截監聽每一個URL Loading System中發出request請求,記住是URL Loading System中那些類發出的請求,也支援AFNetwoking,UIWebView發出的request,NSURLProtocol都可以攔截和監聽。因為WKWebView 在獨立程序裡執行網路請求。一旦註冊 http(s) scheme 後,網路請求將從 Network Process 傳送到 App Process,這樣 NSURLProtocol 才能攔截網路請求。但是在 WebKit2 的設計裡使用 MessageQueue 進行程序之間的通訊,Network Process 會將請求 encode 成一個 Message,然後通過 IPC(程序間通訊) 傳送給 App Process。出於效能的原因,encode 的時候 將HTTPBody 和 HTTPBodyStream 這兩個欄位丟棄掉(坑)因此,如果通過 registerSchemeForCustomProtocol 註冊了 http(s) scheme, 那麼由 WKWebView 發起的所有 http(s)請求都會通過 IPC 傳給主程序 NSURLProtocol 處理,導致 post 請求 body 被清空;//蘋果開源的 WebKit2 原始碼暴露了私有API:+ [WKBrowsingContextController registerSchemeForCustomProtocol:]//通過註冊 http(s) scheme 後 WKWebView 將可以使用 NSURLProtocol 攔截 http(s) 請求:Class cls = NSClassFromString(@"WKBrowsingContextController”); SEL sel = NSSelectorFromString(@"registerSchemeForCustomProtocol:"); if ([(id)cls respondsToSelector:sel]) { // 註冊http(s) scheme, 把 http和https請求交給 NSURLProtocol處理 [(id)cls performSelector:sel withObject:@"http"]; [(id)cls performSelector:sel withObject:@"https"]; }複製程式碼
**說明1:**名目張膽使用私有API,是過不了AppStore稽核的,具體使用什麼辦法,想來你也懂(hun xiao)。
說明2:一旦開啟ATS開關:Allow Arbitrary Loads 選項設定為NO,通過 registerSchemeForCustomProtocol 註冊了 http(s) scheme,WKWebView 發起的所有 http(s) 網路請求將被阻塞(即便將Allow Arbitrary Loads in Web Content 選項設定為YES);
說明3:iOS11之後可以通過WKURLSchemeHandler去完成對WKWebView的請求攔截,不需要再呼叫私有API解決上述問題了。
4、WKWebView自定義資源scheme
向WKWebView 註冊 customScheme, 比如 dynamic://, 而不是https或http,避免對https或http請求的影響保證使用離線包功能的請求,沒有post方式,遇到customScheme請求,比如dynamic://www.dynamicalbumlocalimage.com/,通過 NSURLProtocol 攔截這個請求並載入離線資料。iOS 11上, WebKit 提供的WKURLSchemeHandler可實現攔截,需要注意的只允許開發者攔截自定義 Scheme 的請求,不允許攔截 “http”、“https”、“ftp”、“file” 等的請求,否則會crash。四、其他
1、LocalWebServer
離線包方案中,除了攔截請求載入資源的方式,還有種在專案中搭建local web server,用以獲得本地資源。市面有比較完善的框架CocoaHttpServer (支援iOS、macOS及多種網路場景)GCDWebServer (基於iOS,不支援 https 及 webSocket)Telegraph (Swift實現,功能較上面兩類更完善)複製程式碼具體可參考 基於 LocalWebServer 實現 WKWebView 離線資源載入, 之前團隊有過實踐,採用的是GCDWebServer
2、WKWebView loadRequest 問題
在 WKWebView 上通過 loadRequest 發起的 post 請求 body 資料會丟失://同樣是由於程序間通訊效能問題,HTTPBody欄位被丟棄[request setHTTPMethod:@"POST"];[request setHTTPBody:[@"bodyData" dataUsingEncoding:NSUTF8StringEncoding]];[wkwebview loadRequest: request];複製程式碼
解決:假如想通過-[WKWebView loadRequest:]載入 post 請求 (原始請求)request1: h5.nanhua.com/order/list,可以通過以下步驟實現:
替換請求 scheme,生成新的 post 請求 request2: post://h5.nanhua.com/order/list, 同時將 request1 的 body 欄位複製到 request2 的 header 中(WebKit 不會丟棄 header 欄位);通過-[WKWebView loadRequest:] 載入新的 post 請求 request2;並且通過 +[WKBrowsingContextController registerSchemeForCustomProtocol:]註冊 scheme: post://;註冊 NSURLProtocol 攔截請求 post://h5.nanhua.com/order/list ,替換請求 scheme, 生成新的請求 request3: h5.nanhua.com/order/list,將 request2 header的body 欄位複製到 request3 的 body 中,並使用 NSURLSession 載入 request3,最後將載入結果返回 WKWebView;3、推薦資料
移動端本地 H5 秒開方案探索與實現使用 PageSpeed Insights 進行移動版分析WebView效能、體驗分析與優化iOS app秒開H5優化總結賦予H5以Native的生命 ——《WebView優化》