为什么你做的H5开屏那么慢?H5首屏秒开方案探讨
越來(lái)越多的APP內(nèi)業(yè)務(wù)使用H5的方式實(shí)現(xiàn),怎樣讓H5頁(yè)面啟動(dòng)更快是很多人在探索的技術(shù)點(diǎn),本文梳理了啟動(dòng)過(guò)程中的各個(gè)點(diǎn),分別從前端和客戶(hù)端角度去探討有哪些優(yōu)化方案,供大家參考。
隨著移動(dòng)設(shè)備性能不斷增強(qiáng),web 頁(yè)面的性能體驗(yàn)逐漸變得可以接受,又因?yàn)?web 開(kāi)發(fā)模式的諸多好處(跨平臺(tái),動(dòng)態(tài)更新,減體積,無(wú)限擴(kuò)展),APP 客戶(hù)端里出現(xiàn)越來(lái)越多內(nèi)嵌 web 頁(yè)面(為了配上當(dāng)前流行的說(shuō)法,以下把所有網(wǎng)頁(yè)都稱(chēng)為 H5 頁(yè)面,雖然可能跟 H5 沒(méi)關(guān)系),很多 APP 把一些功能模塊改成用 H5 實(shí)現(xiàn)。
雖然說(shuō) H5 頁(yè)面性能變好了,但如果沒(méi)針對(duì)性地做一些優(yōu)化,體驗(yàn)還是很糟糕的,主要兩部分體驗(yàn):
1.頁(yè)面啟動(dòng)白屏?xí)r間:打開(kāi)一個(gè) H5 頁(yè)面需要做一系列處理,會(huì)有一段白屏?xí)r間,體驗(yàn)糟糕。
2.響應(yīng)流暢度:由于 webkit 的渲染機(jī)制,單線(xiàn)程,歷史包袱等原因,頁(yè)面刷新/交互的性能體驗(yàn)不如原生。
本文先不討論第二點(diǎn),只討論第一點(diǎn),怎樣減少白屏?xí)r間。對(duì) APP 里的一些使用 H5 實(shí)現(xiàn)的功能模塊,怎樣加快它們的啟動(dòng)速度,讓它們啟動(dòng)的體驗(yàn)接近原生。
過(guò)程
為什么打開(kāi)一個(gè) H5 頁(yè)面會(huì)有一長(zhǎng)段白屏?xí)r間?因?yàn)樗隽撕芏嗍虑?#xff0c;大概是:
初始化 webview -> 請(qǐng)求頁(yè)面 -> 下載數(shù)據(jù) -> 解析HTML -> 請(qǐng)求 js/css 資源 -> dom 渲染 -> 解析 JS 執(zhí)行 -> JS 請(qǐng)求數(shù)據(jù) -> 解析渲染 -> 下載渲染圖片
一些簡(jiǎn)單的頁(yè)面可能沒(méi)有 JS 請(qǐng)求數(shù)據(jù) 這一步,但大部分功能模塊應(yīng)該是有的,根據(jù)當(dāng)前用戶(hù)信息,JS 向后臺(tái)請(qǐng)求相關(guān)數(shù)據(jù)再渲染,是常規(guī)開(kāi)發(fā)方式。
一般頁(yè)面在 dom 渲染后能顯示雛形,在這之前用戶(hù)看到的都是白屏,等到下載渲染圖片后整個(gè)頁(yè)面才完整顯示,首屏秒開(kāi)優(yōu)化就是要減少這個(gè)過(guò)程的耗時(shí)。
前端優(yōu)化
上述打開(kāi)一個(gè)頁(yè)面的過(guò)程有很多優(yōu)化點(diǎn),包括前端和客戶(hù)端,常規(guī)的前端和后端的性能優(yōu)化在 PC 時(shí)代已經(jīng)有最佳實(shí)踐,主要的是:
1.降低請(qǐng)求量:合并資源,減少 HTTP 請(qǐng)求數(shù),minify / gzip 壓縮,webP,lazyLoad。
2.加快請(qǐng)求速度:預(yù)解析DNS,減少域名數(shù),并行加載,CDN 分發(fā)。
3.緩存:HTTP 協(xié)議緩存請(qǐng)求,離線(xiàn)緩存 manifest,離線(xiàn)數(shù)據(jù)緩存localStorage。
4.渲染:JS/CSS優(yōu)化,加載順序,服務(wù)端渲染,pipeline。
其中對(duì)首屏啟動(dòng)速度影響最大的就是網(wǎng)絡(luò)請(qǐng)求,所以?xún)?yōu)化的重點(diǎn)就是緩存,這里著重說(shuō)一下前端對(duì)請(qǐng)求的緩存策略。我們?cè)偌?xì)分一下,分成 HTML 的緩存,JS/CSS/image 資源的緩存,以及 json 數(shù)據(jù)的緩存。
HTML 和 JS/CSS/image 資源都屬于靜態(tài)文件,HTTP 本身提供了緩存協(xié)議,瀏覽器實(shí)現(xiàn)了這些協(xié)議,可以做到靜態(tài)文件的緩存。
總的來(lái)說(shuō),就是兩種緩存:
1.詢(xún)問(wèn)是否有更新:根據(jù) If-Modified-Since / ETag 等協(xié)議向后端請(qǐng)求詢(xún)問(wèn)是否有更新,沒(méi)有更新返回304,瀏覽器使用本地緩存。
2.直接使用本地緩存:根據(jù)協(xié)議里的 Cache-Control / Expires 字段去確定多長(zhǎng)時(shí)間內(nèi)可以不去發(fā)請(qǐng)求詢(xún)問(wèn)更新,直接使用本地緩存。
前端能做的最大限度的緩存策略是:HTML 文件每次都向服務(wù)器詢(xún)問(wèn)是否有更新,JS/CSS/Image資源文件則不請(qǐng)求更新,直接使用本地緩存。那 JS/CSS 資源文件如何更新?常見(jiàn)做法是在在構(gòu)建過(guò)程中給每個(gè)資源文件一個(gè)版本號(hào)或hash值,若資源文件有更新,版本號(hào)和 hash 值變化,這個(gè)資源請(qǐng)求的 URL 就變化了,同時(shí)對(duì)應(yīng)的 HTML 頁(yè)面更新,變成請(qǐng)求新的資源URL,資源也就更新了。
json 數(shù)據(jù)的緩存可以用 localStorage 緩存請(qǐng)求下來(lái)的數(shù)據(jù),可以在首次顯示時(shí)先用本地?cái)?shù)據(jù),再請(qǐng)求更新,這都由前端 JS 控制。
這些緩存策略可以實(shí)現(xiàn) JS/CSS 等資源文件以及用戶(hù)數(shù)據(jù)的緩存的全緩存,可以做到每次都直接使用本地緩存數(shù)據(jù),不用等待網(wǎng)絡(luò)請(qǐng)求。但 HTML 文件的緩存做不到,對(duì)于 HTML 文件,如果把 Expires / max-age 時(shí)間設(shè)長(zhǎng)了,長(zhǎng)時(shí)間只使用本地緩存,那更新就不及時(shí),如果設(shè)短了,每次打開(kāi)頁(yè)面都要發(fā)網(wǎng)絡(luò)請(qǐng)求詢(xún)問(wèn)是否有更新,再確定是否使用本地資源,一般前端在這里的策略是每次都請(qǐng)求,這在弱網(wǎng)情況下用戶(hù)感受到的白屏?xí)r間仍然會(huì)很長(zhǎng)。所以 HTML 文件的“緩存”和跟“更新”間存在矛盾。
客戶(hù)端優(yōu)化
接著輪到客戶(hù)端出場(chǎng)了,桌面時(shí)代受限于瀏覽器,H5 頁(yè)面無(wú)法做更多的優(yōu)化,現(xiàn)在 H5 頁(yè)面是內(nèi)嵌在客戶(hù)端 APP 上,客戶(hù)端有更多的權(quán)限,于是客戶(hù)端上可以超出瀏覽器的范圍,做更多的優(yōu)化。
HTML 緩存
先接著緩存說(shuō),在客戶(hù)端有更自由的緩存策略,客戶(hù)端可以攔截 H5 頁(yè)面的所有請(qǐng)求,由自己管理緩存,針對(duì)上述 HTML 文件的“緩存”和“更新”之間的矛盾,我們可以用這樣的策略解決:
1.在客戶(hù)端攔截請(qǐng)求,首次請(qǐng)求 HTML 文件后緩存數(shù)據(jù),第二次不發(fā)請(qǐng)求,直接使用緩存數(shù)據(jù)。
2.什么時(shí)候去請(qǐng)求更新?這個(gè)更新請(qǐng)求可以客戶(hù)端自由控制策略,可以在使用本地緩存打開(kāi)本地頁(yè)面后再在后臺(tái)發(fā)起請(qǐng)求詢(xún)問(wèn)更新緩存,下次打開(kāi)時(shí)生效;也可以在 APP 啟動(dòng)時(shí)或某個(gè)時(shí)機(jī)在后臺(tái)去發(fā)起請(qǐng)求預(yù)更新,提升用戶(hù)訪(fǎng)問(wèn)最新代碼的幾率。
這樣看起來(lái)已經(jīng)比較完美了,HTML 文件在用客戶(hù)端的策略緩存,其余資源和數(shù)據(jù)沿用上述前端的緩存方式,這樣一個(gè) H5 頁(yè)面第二次訪(fǎng)問(wèn)從 HTML 到 JS/CSS/Image 資源,再到數(shù)據(jù),都可以直接從本地讀取,無(wú)需等待網(wǎng)絡(luò)請(qǐng)求,同時(shí)又能保持盡可能的實(shí)時(shí)更新,解決了緩存問(wèn)題,大大提升 H5 頁(yè)面首屏啟動(dòng)速度。
問(wèn)題
上述方案似乎已完整解決緩存問(wèn)題,但實(shí)際上還有很多問(wèn)題:
1.沒(méi)有預(yù)加載:第一次打開(kāi)的體驗(yàn)很差,所有數(shù)據(jù)都要從網(wǎng)絡(luò)請(qǐng)求。
2.緩存不可控:緩存的存取由系統(tǒng) webview 控制,無(wú)法控制它的緩存邏輯,帶來(lái)的問(wèn)題包括:
清理邏輯不可控,緩存空間有限,可能緩存幾張大圖片后,重要的 HTML/JS/CSS 緩存就被清除了。
磁盤(pán) IO 無(wú)法控制,無(wú)法從磁盤(pán)預(yù)加載數(shù)據(jù)到內(nèi)存。
更新體驗(yàn)差:后臺(tái) HTML/JS/CSS 更新時(shí)全量下載,數(shù)據(jù)量大,弱網(wǎng)下載耗時(shí)長(zhǎng)。
無(wú)法防劫持:若 HTML 頁(yè)面被運(yùn)營(yíng)商或其他第三方劫持,將長(zhǎng)時(shí)間緩存劫持的頁(yè)面。
這些問(wèn)題在客戶(hù)端上都是可以被解決的,只不過(guò)有點(diǎn)麻煩,簡(jiǎn)單描述下:
1.可以配置一個(gè)預(yù)加載列表,在APP啟動(dòng)或某些時(shí)機(jī)時(shí)提前去請(qǐng)求,這個(gè)預(yù)加載列表需要包含所需 H5 模塊的頁(yè)面和資源,還需要考慮到一個(gè)H5模塊有多個(gè)頁(yè)面的情況,這個(gè)列表可能會(huì)很大,也需要工具生成和管理這個(gè)預(yù)加載列表。
2.客戶(hù)端可以接管所有請(qǐng)求的緩存,不走 webview 默認(rèn)緩存邏輯,自行實(shí)現(xiàn)緩存機(jī)制,可以分緩存優(yōu)先級(jí)以及緩存預(yù)加載。
3.可以針對(duì)每個(gè) HTML 和資源文件做增量更新,只是實(shí)現(xiàn)和管理起來(lái)比較麻煩。
4.在客戶(hù)端使用 httpdns + https 防劫持。
上面的解決方案實(shí)現(xiàn)起來(lái)十分繁瑣,原因就是各個(gè) HTML 和資源文件很多很分散,管理困難,有個(gè)較好的方案可以解決這些問(wèn)題,就是離線(xiàn)包。
離線(xiàn)包
既然很多問(wèn)題都是文件分散管理困難引起,而我們這里的使用場(chǎng)景是使用 H5 開(kāi)發(fā)功能模塊,那很容易想到把一個(gè)個(gè)功能模塊的所有相關(guān)頁(yè)面和資源打包下發(fā),這個(gè)壓縮包可以稱(chēng)為功能模塊的離線(xiàn)包。使用離線(xiàn)包的方案,可以相對(duì)較簡(jiǎn)單地解決上述幾個(gè)問(wèn)題:
1.可以預(yù)先下載整個(gè)離線(xiàn)包,只需要按業(yè)務(wù)模塊配置,不需要按文件配置,離線(xiàn)包包含業(yè)務(wù)模塊相關(guān)的所有頁(yè)面,可以一次性預(yù)加載。
2.離線(xiàn)包核心文件和頁(yè)面動(dòng)態(tài)的圖片資源文件緩存分離,可以更方便地管理緩存,離線(xiàn)包也可以整體提前加載進(jìn)內(nèi)存,減少磁盤(pán) IO 耗時(shí)。
3.離線(xiàn)包可以很方便地根據(jù)版本做增量更新。
4.離線(xiàn)包以壓縮包的方式下發(fā),同時(shí)會(huì)經(jīng)過(guò)加密和校驗(yàn),運(yùn)營(yíng)商和第三方無(wú)法對(duì)其劫持篡改。
到這里,對(duì)于使用 H5 開(kāi)發(fā)功能模塊,離線(xiàn)包是一個(gè)挺不錯(cuò)的方案了,簡(jiǎn)單復(fù)述一下離線(xiàn)包的方案:
1.后端使用構(gòu)建工具把同一個(gè)業(yè)務(wù)模塊相關(guān)的頁(yè)面和資源打包成一個(gè)文件,同時(shí)對(duì)文件加密/簽名。
2.客戶(hù)端根據(jù)配置表,在自定義時(shí)機(jī)去把離線(xiàn)包拉下來(lái),做解壓/解密/校驗(yàn)等工作。
3.根據(jù)配置表,打開(kāi)某個(gè)業(yè)務(wù)時(shí)轉(zhuǎn)接到打開(kāi)離線(xiàn)包的入口頁(yè)面。
4.攔截網(wǎng)絡(luò)請(qǐng)求,對(duì)于離線(xiàn)包已經(jīng)有的文件,直接讀取
5.離線(xiàn)包數(shù)據(jù)返回,否則走 HTTP 協(xié)議緩存邏輯。
離線(xiàn)包更新時(shí),根據(jù)版本號(hào)后臺(tái)下發(fā)兩個(gè)版本間的 diff 數(shù)據(jù),客戶(hù)端合并,增量更新。
更多優(yōu)化
離線(xiàn)包方案在緩存上已經(jīng)做得差不多了,還可以再配上一些細(xì)節(jié)優(yōu)化:
公共資源包
每個(gè)包都會(huì)使用相同的 JS 框架和 CSS 全局樣式,這些資源重復(fù)在每一個(gè)離線(xiàn)包出現(xiàn)太浪費(fèi),可以做一個(gè)公共資源包提供這些全局文件。
預(yù)加載 webview
無(wú)論是 iOS 還是 Android,本地 webview 初始化都要不少時(shí)間,可以預(yù)先初始化好 webview。這里分兩種預(yù)加載:
1.首次預(yù)加載:在一個(gè)進(jìn)程內(nèi)首次初始化 webview 與第二次初始化不同,首次會(huì)比第二次慢很多。原因預(yù)計(jì)是 webview 首次初始化后,即使 webview 已經(jīng)釋放,但一些多 webview 共用的全局服務(wù)或資源對(duì)象仍沒(méi)有釋放,第二次初始化時(shí)不需要再生成這些對(duì)象從而變快。我們可以在 APP 啟動(dòng)時(shí)預(yù)先初始化一個(gè) webview 然后釋放,這樣等用戶(hù)真正走到 H5 模塊去加載 webview時(shí)就變快了。
2.webview 池:可以用兩個(gè)或多個(gè) webview 重復(fù)使用,而不是每次打開(kāi) H5 都新建 webview。不過(guò)這種方式要解決頁(yè)面跳轉(zhuǎn)時(shí)清空上一個(gè)頁(yè)面,另外若一個(gè) H5 頁(yè)面上 JS 出現(xiàn)內(nèi)存泄漏,就影響到其他頁(yè)面,在 APP 運(yùn)行期間都無(wú)法釋放了。
預(yù)加載數(shù)據(jù)
理想情況下離線(xiàn)包的方案第一次打開(kāi)時(shí)所有 HTML/JS/CSS 都使用本地緩存,無(wú)需等待網(wǎng)絡(luò)請(qǐng)求,但頁(yè)面上的用戶(hù)數(shù)據(jù)還是需要實(shí)時(shí)拉,這里可以做個(gè)優(yōu)化,在 webview 初始化的同時(shí)并行去請(qǐng)求數(shù)據(jù),webview 初始化是需要一些時(shí)間的,這段時(shí)間沒(méi)有任何網(wǎng)絡(luò)請(qǐng)求,在這個(gè)時(shí)機(jī)并行請(qǐng)求可以節(jié)省不少時(shí)間。
具體實(shí)現(xiàn)上,首先可以在配置表注明某個(gè)離線(xiàn)包需要預(yù)加載的 URL,客戶(hù)端在 webview 初始化同時(shí)發(fā)起請(qǐng)求,請(qǐng)求由一個(gè)管理器管理,請(qǐng)求完成時(shí)緩存結(jié)果,然后 webview 在初始化完畢后開(kāi)始請(qǐng)求剛才預(yù)加載的 URL,客戶(hù)端攔截到請(qǐng)求,轉(zhuǎn)接到剛才提到的請(qǐng)求管理器,若預(yù)加載已完成就直接返回內(nèi)容,若未完成則等待。
Fallback
如果用戶(hù)訪(fǎng)問(wèn)某個(gè)離線(xiàn)包模塊時(shí),這個(gè)離線(xiàn)包還沒(méi)有下載,或配置表檢測(cè)到已有新版本但本地是舊版本的情況如何處理?幾種方案:
1.簡(jiǎn)單的方案是如果本地離線(xiàn)包沒(méi)有或不是最新,就同步阻塞等待下載最新離線(xiàn)包。這種用戶(hù)打開(kāi)的體驗(yàn)更差了,因?yàn)殡x線(xiàn)包體積相對(duì)較大。
2.也可以是如果本地有舊包,用戶(hù)本次就直接使用舊包,如果沒(méi)有再同步阻塞等待,這種會(huì)導(dǎo)致更新不及時(shí),無(wú)法確保用戶(hù)使用最新版本。
3.還可以對(duì)離線(xiàn)包做一個(gè)線(xiàn)上版本,離線(xiàn)包里的文件在服務(wù)端有一一對(duì)應(yīng)的訪(fǎng)問(wèn)地址,在本地沒(méi)有離線(xiàn)包時(shí),直接訪(fǎng)問(wèn)對(duì)應(yīng)的線(xiàn)上地址,跟傳統(tǒng)打開(kāi)一個(gè)在線(xiàn)頁(yè)面一樣,這種體驗(yàn)相對(duì)等待下載整個(gè)離線(xiàn)包較好,也能保證用戶(hù)訪(fǎng)問(wèn)到最新。
第三種 Fallback 的方式還帶來(lái)兜底的好處,在一些意外情況離線(xiàn)包出錯(cuò)的時(shí)候可以直接訪(fǎng)問(wèn)線(xiàn)上版本,功能不受影響,此外像公共資源包更新不及時(shí)導(dǎo)致版本沒(méi)有對(duì)應(yīng)上時(shí)也可以直接訪(fǎng)問(wèn)線(xiàn)上版本,是個(gè)不錯(cuò)的兜底方案。
上述幾種方案策略也可以混著使用,看業(yè)務(wù)需求。
使用客戶(hù)端接口
網(wǎng)路和存儲(chǔ)接口如果使用 webkit 的 ajax 和 localStorage 會(huì)有不少限制,難以?xún)?yōu)化,可以在客戶(hù)端提供這些接口給 JS,客戶(hù)端可以在網(wǎng)絡(luò)請(qǐng)求上做像 DNS 預(yù)解析/IP直連/長(zhǎng)連接/并行請(qǐng)求等更細(xì)致的優(yōu)化,存儲(chǔ)也使用客戶(hù)端接口也能做讀寫(xiě)并發(fā)/用戶(hù)隔離等針對(duì)性?xún)?yōu)化。
服務(wù)端渲染
早期 web 頁(yè)面里,JS 只是負(fù)責(zé)交互,所有內(nèi)容都是直接在 HTML 里,到現(xiàn)代 H5 頁(yè)面,很多內(nèi)容已經(jīng)依賴(lài) JS 邏輯去決定渲染什么,例如等待 JS 請(qǐng)求 JSON 數(shù)據(jù),再拼接成 HTML 生成 DOM 渲染到頁(yè)面上,于是頁(yè)面的渲染展現(xiàn)就要等待這一整個(gè)過(guò)程,這里有一個(gè)耗時(shí),減少這里的耗時(shí)也是白屏優(yōu)化的范圍之內(nèi)。
優(yōu)化方法可以是人為減少 JS 渲染邏輯,也可以是更徹底地,回歸到原始,所有內(nèi)容都由服務(wù)端返回的 HTML 決定,無(wú)需等待 JS 邏輯,稱(chēng)之為服務(wù)端渲染。是否做這種優(yōu)化視業(yè)務(wù)情況而定,畢竟這種會(huì)帶來(lái)開(kāi)發(fā)模式變化/流量增大/服務(wù)端開(kāi)銷(xiāo)增大這些負(fù)面影響。手Q的部分頁(yè)面就是使用服務(wù)端渲染的方式,稱(chēng)為動(dòng)態(tài)直出。
最后
從前端優(yōu)化,到客戶(hù)端緩存,到離線(xiàn)包,到更多的細(xì)節(jié)優(yōu)化,做到上述這些點(diǎn),H5 頁(yè)面在啟動(dòng)上差不多可以媲美原生的體驗(yàn)了。
總結(jié)起來(lái),大體優(yōu)化思路就是:緩存/預(yù)加載/并行,緩存一切網(wǎng)絡(luò)請(qǐng)求,盡量在用戶(hù)打開(kāi)之前就加載好所有內(nèi)容,能并行做的事不串行做。這里有些優(yōu)化手段需要做好一整套工具和流程支持,需要跟開(kāi)發(fā)效率權(quán)衡,視實(shí)際需求優(yōu)化。
另外上述討論的是針對(duì)功能模塊類(lèi)的 H5 頁(yè)面秒開(kāi)的優(yōu)化方案,客戶(hù)端 APP 上除了功能模塊,其他一些像營(yíng)銷(xiāo)活動(dòng)/外部接入的 H5 頁(yè)面可能有些優(yōu)化點(diǎn)就不適用,還需要視實(shí)際情況和需求而定。另外微信小程序就是屬于功能模塊的類(lèi)別,差不多是這個(gè)套路。
這里討論了 H5 頁(yè)面首屏啟動(dòng)時(shí)間的優(yōu)化,上述優(yōu)化過(guò)后,基本上耗時(shí)只剩 webview 本身的啟動(dòng)/渲染機(jī)制問(wèn)題了,這個(gè)問(wèn)題跟后續(xù)的響應(yīng)流暢度的問(wèn)題一起屬于另一個(gè)優(yōu)化范圍,就是類(lèi) RN / Weex 這樣的方案,有機(jī)會(huì)再探討。
總結(jié)
以上是生活随笔為你收集整理的为什么你做的H5开屏那么慢?H5首屏秒开方案探讨的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 阿里凑单算法首次公开!打包购商品挖掘系统
- 下一篇: 2018年科技将怎样改变世界?阿里12位