導讀:目前是移動互聯網全面發展的時代,隨着產品迭代速度的不斷提高,網頁在 App 開發中佔據的比例也與日俱增。網頁開發不只能夠較低成本的實現 iOS、Android 和 Web 等多端複用節省人力,還可以有效減小程序安裝包的體積,更重要的是能夠堂而皇之的規避 Apple 對 iOS 端熱更新的封鎖。但另外一方面,移動端網頁相較於原生頁面而言在加載速度方面仍有比較明顯的差距。如何最大程度的減少這種差距,爲用戶提供一個良好的交互體驗就成了每個移動開發者都須要掌握的能力。本文將結合百度愛番番前端團隊在過去一段時間裏的實際研發經歷,爲你們從體驗、性能、安全等方面系統分析並優化解決移動端網頁開發所面臨的一些問題,讓用戶在 App 中打開網頁時可以作到秒開,如原生頁面般流暢。前端
全文5800字,預計閱讀時間 12分鐘。算法
1、明確問題:網頁緩慢
現階段移動端設備相較於傳統的桌面級電腦還有不少不足之處,「 帶寬低 」、「 速度慢 」、「 內存小 」是三個最明顯的瓶頸,而這些卻偏偏是網頁所依賴的重點。後端
其中首當其衝的就是網絡條件,儘管近年來伴隨着 4G、5G 的普及用戶手機的網速不斷提高,可是移動端的網絡延遲永遠是不肯定的,它會受到各類條件的限制,現實生活中仍然會有不少狀況下會致使用戶的網速不佳。而這種制約對網頁而言是十分嚴重的,它會使得網頁加載過程變得更加漫長,甚至是失敗。跨域
另外一個方面是處理器的速度,現今的網頁承載的信息愈來愈多,界面交互和業務邏輯也愈來愈複雜,過多的計算量會讓網頁的處理時間增長。而用戶設備的硬件配置又是多種多樣的,這個問題在其中佔大多數的中低端機型上會更加明顯。瀏覽器
對於網頁而言,設備的內存大小也很重要的,更大的內存表明能夠支持更多的網頁內容。反以內存緊張會讓 APP 在處理網頁時變得效率低下,頻繁出現卡頓問題,最致命的是會更容易引起 OOM( Out Of Memory )現象致使程序崩潰。緩存
隨着技術的發展,移動端交互體驗的不斷提高,人們對網頁加載緩慢的忍耐度也愈來愈低。有調查代表,超過 2/3 的用戶認爲對於網頁來講加載速度是影響瀏覽體驗最大的一個因素。當移動端的網頁加載時間超過3秒,過半的用戶會選擇直接離開。因此一個快速的加載過程,是咱們提升 APP 網頁質量的重要一環。安全
2、分析痛點:加載耗時
在討論如何提高網頁加載速度前,須要先以數字的形式給出網頁加載緩慢的定義,明確一個基準點——如何定義用戶所感覺到的網頁加載耗時。這裏有一個計算公式:性能優化
網頁加載耗時 = 網頁加載完成的時間 – 頁面開始加載的時間服務器
其中頁面開始加載的時間比較容易判斷,從用戶的角度來看,當他在上級頁面點擊某處跳轉網頁的時候就能夠理解爲頁面開始加載了。網絡
關鍵是如何界定網頁加載完成的時間,從客戶端開發的方向來講,不管是 iOS 仍是 Android,做爲承載網頁的 WebView 控件,都有一個 loadFinish 回調錶示網頁加載完成,可是實際上它並不能真實反應用戶的實際感官體驗。
這裏咱們先來梳理一下移動端加載一個普通網頁大體須要通過哪些步驟:
因而可知,用戶在打開網頁的整個過程當中前後會經歷 無反饋、白屏、loading 這幾個階段,而在 WebView 控件 loadFinish 後,頁面基本上還停留在 loading 界面。因此上面公式裏提到的網頁加載完成通常能夠理解爲業務數據渲染完成的時候,由於只有在這以後用戶纔可以真正看見想要的內容。
換言之網頁加載緩慢體如今數值上來講就是指用戶點擊開啓網頁到業務數據渲染完成這段時間差過大,那麼如何下降這個時間差就是咱們亟待解決的問題。
3、提供方案:優化實踐
針對移動端網頁加載時無反饋、白屏、loading 這三個階段,愛番番前端團隊從前面提到的網絡條件、處理速度、內存佔用這幾個點進行切入,針對緩存系統、網頁渲染機制、瀏覽器內核、網絡請求效率等方向,制定了一系列的優化方案。
首先是 「 獨立組件打包分發 」 ,其後在此基礎之上前後進行了 「 頁面按需預先渲染 」、「 網頁容器預初始化 」 和 「 業務請求前置執行 」 等處理。
咱們的目標是使愛番番內各主要網頁的加載耗時下降到 1s 之內。
3.1 獨立組件打包分發
一般來講,加載網頁時靜態資源的下載是很是耗時的,並且這個過程也是最容易受到網絡環境影響的。爲了解決這個問題,咱們將一組獨立網頁的 HTML、JavaScript、CSS 等靜態資源壓縮打包,造成一個離線組件包,在 App 啓動後預先下載並解壓到手機本地,當用戶打開目標網頁時,直接從本地加載這些資源。
並且一個 App 中根據業務模塊能夠分爲多個離線組件包,每一個離線包都擁有惟一的版本號,經過後端搭建的離線包平臺進行管理和下發,客戶端會在指定的時機和平臺同步離線包的版本信息,當有版本更新的時候,會在後臺批量靜默下載並更新本地文件,用戶在正常操做 App 時基本處在一個無感知的狀態。
經過獨立組件打包分發的方案能夠繞過耗時的靜態資源下載環節,網頁加載過程當中的白屏時間也可以獲得大幅下降。
3.2 頁面按需預先渲染
頁面按需預先渲染是爲了一次性解決網頁加載過程當中各個環節問題所制定的優化方案,它基於客戶端渲染( NSR,Native Side Rending )的思想實現,而 NSR 又是由服務端渲染( SSR,Server Side Rendering )引伸而來的,NSR 的本質是分佈式的 SSR。
SSR 是指在服務端完成網頁的渲染,在服務端完成頁面模板、數據填充、頁面排版等工做,而後將完整的 HTML 內容返回給瀏覽器。因爲全部的渲染工做都在服務端完成,所以網頁加載耗時會有所下降。可是這種優化方案致使前端頁面的渲染須要在服務端完成,並不能很好進行先後端職責分離,並且頁面加載過程當中不可避免仍會有一段白屏時間,同時對於服務端的負載要求也會比較高。
因此這裏咱們採用了 NSR 的方式,在用戶登陸成功後,藉助 WebView 控件啓用一個 JS-Runtime ,在用戶手動跳轉目標網頁以前提早在後臺加載本地離線組件包中的資源併發送網絡請求獲取業務數據,再進行排版和渲染,動態直出,最後將網頁設置到內存級別的 MemoryCache 中,從而達到點開即看的效果。退一步說,即使用戶在點開頁面時以上流程並未所有執行完畢,也會由於提早執行了其中部分流程,較傳統模式下降一些用戶感知時間。
可是另外一方面,預先渲染也是一柄雙刃劍,它本質上是利用空間換取時間,會佔用大量額外的內存空間。但內存在一些較低端的移動設備上是十分寶貴的,太高的內存佔用會引起一系列的體驗和穩定性的問題。因此如何在儘量低的內存佔用狀況下完成預先渲染,是須要仔細權衡的。最終咱們決定按需只對 App 內入口級的幾個重要頁面開放了此功能,儘可能避免佔用太高的內存空間。
頁面按需預先渲染的收益是十分顯著的,經數據統計,目標頁面的平均網頁加載耗時 iOS 從2500ms 下降到了 231ms ,Android 從 2803ms 下降到了 628ms。
3.3 網頁容器預初始化
移動端和 Web 端網頁的加載過程並不徹底一致,當App啓動時默認是不會自動初始化內嵌瀏覽器內核的,只有看成爲網頁容器的 WebView 初始化時纔會執行。因此針對這一點咱們設計了網頁容器預初始化的優化方案。
3.3.1 容器預加載
容器預加載是網頁容器預初始化方案的核心,即在用戶開啓網頁前預先進行 WebView 控件的初始化以及相關資源和框架的加載以下降網頁加載耗時。
愛番番在下載和更新離線組件包後,會在後臺初始化 WebView 控件,並加載組件包內的一箇中間態網頁,提早加載相關資源和框架,中間頁加載成功後 WebView 會被放置在容器池中,開始監聽一個自定的 JS 方法並進行等待。
當用戶點擊開啓目標網頁時,會先根據所在離線組件包內的配置文件,判斷該該頁面是否開啓了容器預加載的功能,若是開啓了會向容器池請求獲取初始化好的 WebView,獲取成功後調用自定的 JS 方法通知 H5 端,最後 H5 端經過 Vue Router 跳轉目標頁面。
而且在容器池向外交付 WebView 時,會自動從新初始化一個新的 WebView 開始加載中間頁,爲下一次用戶操做作準備。
由於在容器池中獲取的 WebView 已經提早進行了初始化,而且完成了組件包內一些公共資源和框架的加載,因此在當用戶開啓網頁時所見到的白屏階段就會大幅縮短,網頁加載耗時也會顯著下降。具體體如今數據方面,使用該方案優化的網頁在 iOS 和 Android 雙端加載速度均提高了 200~300ms 。
3.3.2 微前端架構
在前面的 「 容器預加載 」 方案中,由於各業務離線組件包內的頁面間是相互獨立的,沒法經過Vue Router 跳轉至其餘組件包內的頁面,因此須要在容器池中爲每一個業務組件包都提供一個WebView 控件,用於加載中間態網頁。隨着業務組件包數量的不斷增多,容器池中的 WebView 也會同步增多,如此會大幅提升內存佔用,而如前文所說,較高的內存佔用可能會引起程序運行卡頓,甚至崩潰等問題,這在較低端的設備上是尤爲致命的。
而微前端方案則很好的解決了這個難題,所謂的微前端主要是將原先的多個業務離線組件包聚合成爲了一個系統,實現系統內的總體調度,完成組件包間的交互。愛番番採用的是 Master-Slave 架構,即主-從式設計:
Master:公共組件包,負責加載其餘組件包,而且提供公共資源;
Slave:各業務組件包,負責不一樣模塊的具體業務代碼。
其中 Master 和 Slave 之間的數據交互在本地主要依賴 Symbolic Link 實現,Native 端會爲包括公共組件包在內的每一個離線組件包提供一個對應所在本地路徑的 Symbolic Link。容器池中僅爲公共組件包提供一個 WebView 控件,而公共組件包能夠經過 Symbolic Link 進行本地尋址,找到對應業務組件包內頁面的路徑,再使用 Vue Router 就能夠完成 「 容器預加載 」 中的跳轉邏輯。這樣一來就在原有 n個 組件包的條件下,將組件容器池中的 WebView 控件從 n個縮減爲了 1個,內存佔用也縮減到了原來的 1/n,有效下降了程序的卡頓率和崩潰率。
另外一方面,公共組件包也將各業務組件包內的一些公共框架資源提取了出來,如 Vue Router等,各業務組件包在使用它們時,一樣能夠經過 Symbolic Link 定位到公共組件包中的對應框架資源。這樣作的好處在於能夠對公共資源進行統一管理,並在必定程度上下降了離線組件包總體的體積。
經過微前端架構的優化,使得咱們的 App 在展現網頁時明顯下降了內存佔用,避免了不少高內存帶來的問題,並且各業務離線組件包的體積也都有所縮小。
3.3.3 預置離線包
因前面提到的 「 微前端架構 」 中採用了主-從式設計,做爲 Master 的公共離線組件包內包含了業務離線組件包( Slave )所須要的一些公共框架資源。當用戶打開業務組件包的某個頁面時,公共組件包的存在就成爲了這個頁面能正常運行的前提條件,而當用戶初次安裝啓動 APP時,一定有一個從離線包平臺下載公共組件包的過程,若是下載過慢會致使期間其餘全部業務組件包都沒法正常使用。
爲了不此類問題的出現,咱們採用了將公共離線組件包預置進 APP 安裝包內的方式來確保其優先性,而且它會隨着 App 發版進行更新。APP 在初次安裝啓動後,通常會跳過預置包的下載流程,將其直接從 APP 複製到本地沙盒中。
並且另外一方面,預置離線組件包的方案不只適用於公共組件包,也適用於業務組件包,尤爲對於其中一些體積較大下載耗時較長的包,能夠在 APP 初次安裝啓動時爲用戶提供更加良好的交互體驗。
3.4 業務請求前置執行
網頁大多須要依賴服務器提供業務數據驅動頁面展現內容。在前面分析網頁加載耗時的過程當中能夠得知,在傳統模式下業務網絡請求要在 WebView 容器 loadFinish 後纔會執行,針對這一點咱們設計了業務請求前置執行的優化方案。
3.4.1 客戶端請求
爲了支持業務請求前置執行的方案,首先須要對網頁中的網絡請求進行客戶端化改造,即由Native 端來處理網頁中的業務數據網絡請求。而且用客戶端請求和服務器進行交互,還能夠解決原先使用 XHR 請求的一系列相關問題,好比跨域限制、測試聯調時沒法直連後端,網絡層配置邏輯不統一等。
具體步驟方面,在網頁進行網絡請求時,首先由 H5 端配置業務請求信息,如請求地址、接口入參、自定義請求頭等,並經過 JS-Bridge 將這些內容發送到 Native 端,Native 端再執行一些網絡層的統一配置和優化後發送請求,好比添加 Cookie 和一些必要的請求頭數據等。最後在收到 response 後再次經過 JS-Bridge 將內容返回給 H5 端。
這樣一方面使得前端開發人員在調試時不用作任何配置和代理便可直連服務器,避免了傳統模式下耗時的發包流程,大幅提高了迭代開發的效率。另外一方面,iOS 端的 WKWebView 增強了安全性限制,在訪問本地網頁時禁止跨域請求,使用客戶端請求能夠完美規避這個限制。最後它還能夠將網頁中全部的請求都在Native端進行集中管理和統一優化,這也爲以後的「網絡預加載」的實現提供了前提條件。
3.4.2 網絡預加載
優化前(圖1):
優化後(圖2):
網絡預加載是業務請求前置執行方案的核心。通常來講,網頁中的業務網絡請求最先能夠在在頁面構建 DOM 完成後執行,即 圖1 中的 B點之後,而在愛番番中,大部分網頁的業務請求相關參數都依賴於一些頁面級的入參,可是頁面入參須要等待本地 JS 腳本( 愛番番 App 內置的 JS 文件,其中包含 H5 端和 Native 端間交互的衆多邏輯,是網頁正常加載的前提條件 )注入完成以後才能夠獲取,又由於本地JS腳本須要在 loadFinish( 即 圖1 中的 B點 )以後才執行注入,因此對咱們的大多數網頁來講,發送業務請求的最先時機在 圖1 中的 C點 之後。而且按前文 「 客戶端請求 」 的技術方案,網頁發送網絡請求須要先由 H5端 配置請求信息,再經過 JS-Bridge 申請 Native 端來真正執行。因此在進行網絡預加載優化之前,業務網絡請求的發送時機爲 圖1 中的 D點。
這裏總結一下,傳統方式下網頁加載耗時能夠簡單分爲兩個部分:
一、解析靜態資源並構建 DOM 結構和本地 JS 腳本注入( 即 A-D ):這裏咱們將此部分稱爲 Part1,它的耗時主要取決於前端;
二、業務網絡請求的具體執行過程( 即 D-F ):這裏咱們將此部分稱爲 Part2,它的耗時主要取決於用戶的網絡環境和後端;
Part1 和 Part2 兩者之間是先後串行的關係。那麼咱們爲了儘量的下降用戶感知時間,能夠將 Part2 儘可能前置,將其與 Part1 作並行處理。
網絡預加載正是基於這種思想實現的,首先前端開發人員會先將要執行預加載的網絡請求的相關信息寫入對應離線組件包的配置文件當中,當用戶打開該離線包的指定網頁時,會在 Native端直接從組件包的配置信息中讀取信息並當即開始發送網絡請求。這個動做會在子線程中執行,幾乎和 WebView 控件的初始化同時開始。
而以後獲取 response 的時機(即圖2中的I點)根據 Part2 耗時的不一樣,大致可分爲2種狀況:
一、在Part1 完成以前(即I點在J點以前、G點以後,如圖2所示):這時 Native 端會將網絡預加載獲取到的 response 進行緩存並開始等待,當 Part1 完成後 Native 端會收到H5端以正常方式發來的同一請求的申請,此時當即經過 JS-Bridge 將緩存的 response 交付給H5端使用,並銷燬緩存。另外一方面,若等待時長超過必定時間限制,Native端也會銷燬緩存,視做這次網絡預加載行爲失敗。若是成功,這種狀況下的收益爲整個 Part2 的耗時(D-F);
二、在 Part1 完成以後(即I點在J點以後):這種狀況下,當網絡預加載的請求仍在途中時,Native 端就收到了 H5 端正常方式發來的請求申請,此時 Native 端會攔截本次的請求申請,繼續等待前面網絡預加載的請求返回,待返回後再經過 JS-Bridge 將 response 交付給 H5端使用。這種狀況下的收益爲 Part1 的耗時(A-D)。
以上就是網絡預加載的主要實現原理,經過這種優化方案,在用戶打開移動端網頁時,能夠很大程度的下降用戶在開啓網頁時的可感知時長,體如今數據方面,愛番番內使用此方案的網頁加載耗時下降了 300 ~ 600ms 。
4、總結收益:持續探索
除了以上提到的這些,百度愛番番前端團隊爲了下降網頁加載耗時還作了許多工做,好比 iOS升級 WKWebView、Android 升級 X5 內核等等,限於篇幅緣由,在此就不作展開了。
經過這些優化手段,愛番番移動端各一級核心頁面的加載耗時從平均 2-3秒 收斂到了 1秒 之內,基本上達到了秒開的既定目標。
固然,咱們將來須要作的還有不少,後續也會持續推動移動端網頁的性能優化工做,讓用戶在使用愛番番時可以享受到接近原生頁面的體驗。
本期做者 | 時恩寶貝,百度愛番番前端高級工程師,擁有多年研發經歷。擅長iOS、Android、Web多端開發。
招聘信息:
不管你是後端,前端 ,大數據仍是算法,這裏有若干職位在等你,歡迎投遞簡歷, 愛番番業務部期待你的加入!
簡歷投遞郵箱:geektalk@baidu.com (投遞備註【愛番番】)
推薦閱讀:
---------- END ----------
百度Geek說
百度官方技術公衆號上線啦!
技術乾貨 · 行業資訊 · 線上沙龍 · 行業大會
招聘信息 · 內推信息 · 技術書籍 · 百度周邊
歡迎各位同窗關注