天天看點

移動H5首屏秒開優化方案

随着移動裝置性能不斷增強,web 頁面的性能體驗逐漸變得可以接受,又因為 web 開發模式的諸多好處(跨平台,動态更新,減體積,無限擴充),APP 用戶端裡出現越來越多内嵌 web 頁面(為了配上目前流行的說法,以下把所有網頁都稱為 H5 頁面,雖然可能跟 H5 沒關系),很多 APP 把一些功能子產品改成用 H5 實作。

移動H5首屏秒開優化方案

  雖然說 H5 頁面性能變好了,但如果沒針對性地做一些優化,體驗還是很糟糕的,主要兩部分體驗:

  頁面啟動白屏時間:打開一個 H5 頁面需要做一系列處理,會有一段白屏時間,體驗糟糕。

  響應流暢度:由于 webkit 的渲染機制,單線程,曆史包袱等原因,頁面重新整理/互動的性能體驗不如原生。

  本文先不讨論第二點,隻讨論第一點,怎樣減少白屏時間。對 APP 裡的一些使用 H5 實作的功能子產品,怎樣加快它們的啟動速度,讓它們啟動的體驗接近原生。

  過程

  為什麼打開一個 H5 頁面會有一長段白屏時間?因為它做了很多事情,大概是:

  初始化 webview -> 請求頁面 -> 下載下傳資料 -> 解析HTML -> 請求 js/css 資源 -> dom 渲染 -> 解析 JS 執行 -> JS 請求資料 -> 解析渲染 -> 下載下傳渲染圖檔

  一些簡單的頁面可能沒有 JS 請求資料 這一步,但大部分功能子產品應該是有的,根據目前使用者資訊,JS 向背景請求相關資料再渲染,是正常開發方式。

  一般頁面在 dom 渲染後能顯示雛形,在這之前使用者看到的都是白屏,等到下載下傳渲染圖檔後整個頁面才完整顯示,首屏秒開優化就是要減少這個過程的耗時。

  前端優化

  上述打開一個頁面的過程有很多優化點,包括前端和用戶端,正常的前端和後端的性能優化在桌面時代已經有最佳實踐,主要的是:

  降低請求量:合并資源,減少 HTTP 請求數,minify / gzip 壓縮,webP,lazyLoad。

  加快請求速度:預解析DNS,減少域名數,并行加載,CDN 分發。

  緩存:HTTP 協定緩存請求,離線緩存 manifest,離線資料緩存localStorage。

  渲染:JS/CSS優化,加載順序,服務端渲染,pipeline。

  其中對首屏啟動速度影響最大的就是網絡請求,是以優化的重點就是緩存,這裡着重說一下前端對請求的緩存政策。我們再細分一下,分成 HTML 的緩存,JS/CSS/image 資源的緩存,以及 json 資料的緩存。

  HTML 和 JS/CSS/image 資源都屬于靜态檔案,HTTP 本身提供了緩存協定,浏覽器實作了這些協定,可以做到靜态檔案的緩存,具體可以參考這裡,總的來說,就是兩種緩存:

  詢問是否有更新:根據 If-Modified-Since / ETag 等協定向後端請求詢問是否有更新,沒有更新傳回304,浏覽器使用本地緩存。

  直接使用本地緩存:根據協定裡的 Cache-Control / Expires 字段去确定多長時間内可以不去發請求詢問更新,直接使用本地緩存。

  前端能做的最大限度的緩存政策是:HTML 檔案每次都向伺服器詢問是否有更新,JS/CSS/Image資源檔案則不請求更新,直接使用本地緩存。那 JS/CSS 資源檔案如何更新?常見做法是在在建構過程中給每個資源檔案一個版本号或hash值,若資源檔案有更新,版本号和 hash 值變化,這個資源請求的 URL 就變化了,同時對應的 HTML 頁面更新,變成請求新的資源URL,資源也就更新了。

  json 資料的緩存可以用 localStorage 緩存請求下來的資料,可以在首次顯示時先用本地資料,再請求更新,這都由前端 JS 控制。

  這些緩存政策可以實作 JS/CSS 等資源檔案以及使用者資料的緩存的全緩存,可以做到每次都直接使用本地緩存資料,不用等待網絡請求。但 HTML 檔案的緩存做不到,對于 HTML 檔案,如果把 Expires / max-age 時間設長了,長時間隻使用本地緩存,那更新就不及時,如果設短了,每次打開頁面都要發網絡請求詢問是否有更新,再确定是否使用本地資源,一般前端在這裡的政策是每次都請求,這在弱網情況下使用者感受到的白屏時間仍然會很長。是以 HTML 檔案的“緩存”和跟“更新”間存在沖突。

  用戶端優化

  接着輪到用戶端出場了,桌面時代受限于浏覽器,H5 頁面無法做更多的優化,現在 H5 頁面是内嵌在用戶端 APP 上,用戶端有更多的權限,于是用戶端上可以超出浏覽器的範圍,做更多的優化。

  HTML 緩存

  先接着緩存說,在用戶端有更自由的緩存政策,用戶端可以攔截 H5 頁面的所有請求,由自己管理緩存,針對上述 HTML 檔案的“緩存”和“更新”之間的沖突,我們可以用這樣的政策解決:

  在用戶端攔截請求,首次請求 HTML 檔案後緩存資料,第二次不發請求,直接使用緩存資料。

  什麼時候去請求更新?這個更新請求可以用戶端自由控制政策,可以在使用本地緩存打開本地頁面後再在背景發起請求詢問更新緩存,下次打開時生效;也可以在 APP 啟動時或某個時機在背景去發起請求預更新,提升使用者通路最新代碼的幾率。

  這樣看起來已經比較完美了,HTML 檔案在用用戶端的政策緩存,其餘資源和資料沿用上述前端的緩存方式,這樣一個 H5 頁面第二次通路從 HTML 到 JS/CSS/Image 資源,再到資料,都可以直接從本地讀取,無需等待網絡請求,同時又能保持盡可能的實時更新,解決了緩存問題,大大提升 H5 頁面首屏啟動速度。建立一個前端學習qun438905713,在群裡大多數都是零基礎學習者,大家互相幫助,互相解答,并且還準備很多學習資料,歡迎零基礎的小夥伴來一起交流。

  問題

  上述方案似乎已完整解決緩存問題,但實際上還有很多問題:

  沒有預加載:第一次打開的體驗很差,所有資料都要從網絡請求。

  緩存不可控:緩存的存取由系統 webview 控制,無法控制它的緩存邏輯,帶來的問題包括: i. 清理邏輯不可控,緩存空間有限,可能緩存幾張大圖檔後,重要的 HTML/JS/CSS 緩存就被清除了。 ii.磁盤 IO 無法控制,無法從磁盤預加載資料到記憶體。

  更新體驗差:背景 HTML/JS/CSS 更新時全量下載下傳,資料量大,弱網下載下傳耗時長。

  無法防劫持:若 HTML 頁面被營運商或其他第三方劫持,将長時間緩存劫持的頁面。

  這些問題在用戶端上都是可以被解決的,隻不過有點麻煩,簡單描述下:

  可以配置一個預加載清單,在APP啟動或某些時機時提前去請求,這個預加載清單需要包含所需 H5 子產品的頁面和資源,還需要考慮到一個H5子產品有多個頁面的情況,這個清單可能會很大,也需要工具生成和管理這個預加載清單。

  用戶端可以接管所有請求的緩存,不走 webview 預設緩存邏輯,自行實作緩存機制,可以分緩存優先級以及緩存預加載。

  可以針對每個 HTML 和資源檔案做增量更新,隻是實作和管理起來比較麻煩。

  在用戶端使用 httpdns + https 防劫持。

  上面的解決方案實作起來十分繁瑣,原因就是各個 HTML 和資源檔案很多很分散,管理困難,有個較好的方案可以解決這些問題,就是離線包。

  離線包

  既然很多問題都是檔案分散管理困難引起,而我們這裡的使用場景是使用 H5 開發功能子產品,那很容易想到把一個個功能子產品的所有相關頁面和資源打包下發,這個壓縮包可以稱為功能子產品的離線包。使用離線包的方案,可以相對較簡單地解決上述幾個問題:

  可以預先下載下傳整個離線包,隻需要按業務子產品配置,不需要按檔案配置,離線包包含業務子產品相關的所有頁面,可以一次性預加載。

  離線包核心檔案和頁面動态的圖檔資源檔案緩存分離,可以更友善地管理緩存,離線包也可以整體提前加載進記憶體,減少磁盤 IO 耗時。

  離線包可以很友善地根據版本做增量更新。

  離線包以壓縮包的方式下發,同時會經過加密和校驗,營運商和第三方無法對其劫持篡改。

  到這裡,對于使用 H5 開發功能子產品,離線包是一個挺不錯的方案了,簡單複述一下離線包的方案:

  後端使用建構工具把同一個業務子產品相關的頁面和資源打包成一個檔案,同時對檔案加密/簽名。

  用戶端根據配置表,在自定義時機去把離線包拉下來,做解壓/解密/校驗等工作。

  根據配置表,打開某個業務時轉接到打開離線包的入口頁面。

  攔截網絡請求,對于離線包已經有的檔案,直接讀取離線包資料傳回,否則走 HTTP 協定緩存邏輯。

  離線包更新時,根據版本号背景下發兩個版本間的 diff 資料,用戶端合并,增量更新。

  更多優化

  離線包方案在緩存上已經做得差不多了,還可以再配上一些細節優化:

  公共資源包

  每個包都會使用相同的 JS 架構和 CSS 全局樣式,這些資源重複在每一個離線包出現太浪費,可以做一個公共資源包提供這些全局檔案。

  預加載 webview

  無論是 iOS 還是 Android,本地 webview 初始化都要不少時間,可以預先初始化好 webview。這裡分兩種預加載:

  首次預加載:在一個程序内首次初始化 webview 與第二次初始化不同,首次會比第二次慢很多。原因預計是 webview 首次初始化後,即使 webview 已經釋放,但一些多 webview 共用的全局服務或資源對象仍沒有釋放,第二次初始化時不需要再生成這些對象進而變快。我們可以在 APP 啟動時預先初始化一個 webview 然後釋放,這樣等使用者真正走到 H5 子產品去加載 webview時就變快了。

  webview 池:可以用兩個或多個 webview 重複使用,而不是每次打開 H5 都建立 webview。不過這種方式要解決頁面跳轉時清空上一個頁面,另外若一個 H5 頁面上 JS 出現記憶體洩漏,就影響到其他頁面,在 APP 運作期間都無法釋放了。

  可以參考美團點評的這篇文章。

  預加載資料

  理想情況下離線包的方案第一次打開時所有 HTML/JS/CSS 都使用本地緩存,無需等待網絡請求,但頁面上的使用者資料還是需要實時拉,這裡可以做個優化,在 webview 初始化的同時并行去請求資料,webview 初始化是需要一些時間的,這段時間沒有任何網絡請求,在這個時機并行請求可以節省不少時間。

  具體實作上,首先可以在配置表注明某個離線包需要預加載的 URL,用戶端在 webview 初始化同時發起請求,請求由一個管理器管理,請求完成時緩存結果,然後 webview 在初始化完畢後開始請求剛才預加載的 URL,用戶端攔截到請求,轉接到剛才提到的請求管理器,若預加載已完成就直接傳回内容,若未完成則等待。

  Fallback

  如果使用者通路某個離線包子產品時,這個離線包還沒有下載下傳,或配置表檢測到已有新版本但本地是舊版本的情況如何處理?幾種方案:

  簡單的方案是如果本地離線包沒有或不是最新,就同步阻塞等待下載下傳最新離線包。這種使用者打開的體驗更差了,因為離線包體積相對較大。

  也可以是如果本地有舊包,使用者本次就直接使用舊包,如果沒有再同步阻塞等待,這種會導緻更新不及時,無法確定使用者使用最新版本。

  還可以對離線包做一個線上版本,離線包裡的檔案在服務端有一一對應的通路位址,在本地沒有離線包時,直接通路對應的線上位址,跟傳統打開一個線上頁面一樣,這種體驗相對等待下載下傳整個離線包較好,也能保證使用者通路到最新。

  第三種 Fallback 的方式還帶來兜底的好處,在一些意外情況離線包出錯的時候可以直接通路線上版本,功能不受影響,此外像公共資源包更新不及時導緻版本沒有對應上時也可以直接通路線上版本,是個不錯的兜底方案。

  上述幾種方案政策也可以混着使用,看業務需求。

  使用用戶端接口

  網路和存儲接口如果使用 webkit 的 ajax 和 localStorage 會有不少限制,難以優化,可以在用戶端提供這些接口給 JS,用戶端可以在網絡請求上做像 DNS 預解析/IP直連/長連接配接/并行請求等更細緻的優化,存儲也使用用戶端接口也能做讀寫并發/使用者隔離等針對性優化。建立一個前端學習qun438905713,在群裡大多數都是零基礎學習者,大家互相幫助,互相解答,并且還準備很多學習資料,歡迎零基礎的小夥伴來一起交流。

  服務端渲染

  早期 web 頁面裡,JS 隻是負責互動,所有内容都是直接在 HTML 裡,到現代 H5 頁面,很多内容已經依賴 JS 邏輯去決定渲染什麼,例如等待 JS 請求 JSON 資料,再拼接成 HTML 生成 DOM 渲染到頁面上,于是頁面的渲染展現就要等待這一整個過程,這裡有一個耗時,減少這裡的耗時也是白屏優化的範圍之内。

  優化方法可以是人為減少 JS 渲染邏輯,也可以是更徹底地,回歸到原始,所有内容都由服務端傳回的 HTML 決定,無需等待 JS 邏輯,稱之為服務端渲染。是否做這種優化視業務情況而定,畢竟這種會帶來開發模式變化/流量增大/服務端開銷增大這些負面影響。手Q的部分頁面就是使用服務端渲染的方式,稱為動态直出,見文章。

  最後

  從前端優化,到用戶端緩存,到離線包,到更多的細節優化,做到上述這些點,H5 頁面在啟動上差不多可以媲美原生的體驗了。

  總結起來,大體優化思路就是:緩存/預加載/并行,緩存一切網絡請求,盡量在使用者打開之前就加載好所有内容,能并行做的事不串行做。這裡有些優化手段需要做好一整套工具和流程支援,需要跟開發效率權衡,視實際需求優化。

  另外上述讨論的是針對功能子產品類的 H5 頁面秒開的優化方案,用戶端 APP 上除了功能子產品,其他一些像營銷活動/外部接入的 H5 頁面可能有些優化點就不适用,還需要視實際情況和需求而定。另外微信小程式就是屬于功能子產品的類别,差不多是這個套路。

  這裡讨論了 H5 頁面首屏啟動時間的優化,上述優化過後,基本上耗時隻剩 webview 本身的啟動/渲染機制問題了,這個問題跟後續的響應流暢度的問題一起屬于另一個優化範圍,就是類 RN / Weex 這樣的方案,有機會再探讨。