開篇以前先介紹一下場景。信息流是一個基於用戶興趣使用算法將用戶感興趣的新聞內容推薦給用戶的一種業務。這種業務帶有很是特點的場景就是用戶有一個「永遠」都刷不完的推薦流列表,點擊列表中的新聞以後能夠跳轉到其詳情頁中查看新聞的正文內容。列表通常都是由客戶端原生去實現的,而詳情頁這塊因爲新聞內容結構的複雜性,通常仍是會使用 h5 來實現。這樣就對咱們 h5 的性能提出了要求,咱們必須在用戶切換的時候將切換的白屏時間儘可能減小,這樣才能提升用戶的閱讀體驗。html
本文就將爲你們講述一下咱們是如何實現性能優化達到「閃開」的效果的。咱們能夠先看看效果,下圖左邊是正常版本,而右邊的是優化後的版本。對比之下能夠發現即便我已經悄咪咪的先點擊左邊的手機,同一篇新聞右邊的打開速度明顯比左邊的要快不少。接下來就讓咱們看看這個是如何作到的吧!前端
衆所周知,網頁中內容渲染每每根據渲染方式能夠分爲後渲染和前端渲染兩種方式,最近幾年由前端渲染又演化出了同構渲染,也就是你們常常說的 SSR。這幾種渲染方式的主要優缺點大概整理了主要有以下幾個方面。java
固然本篇文章不是來說各類渲染方式的優缺點的,主要是說由於種種緣由咱們的項目最後使用了前端 JS 渲染的方式。而 JS 渲染帶來的性能問題主要是因爲數據接口請求返回以及前端 JS 資源獲取所帶來的網絡問題。爲了解決這兩個問題,一方面咱們採用了服務端將數據注入到頁面全局變量中的方式避免了數據請求,另外一方面咱們使用了 localStorage 緩存的方式將前端資源作了 LS 緩存避免了二次打開以後的前端資源請求,從而提升了前端渲染的首屏性能。android
雖然咱們避免了前端渲染的一些問題對首屏的性能作了優化,但還遠遠不夠。那目前還有哪些點能夠進行優化呢?簡單的整理了下能夠有以下兩個方面:web
從上面兩個優化點咱們能夠看到全部的優化仍是網絡的優化,主要仍是在移動端網絡對性能的影響是遠遠大於其餘方面的。那麼是否有什麼方案可以讓咱們免去這些網絡請求呢,最終咱們給的答案就是詳情頁本地化。經過本地化方案,咱們將平均 820ms 的首屏渲染時間優化到了 260ms,整整提升了三倍多!算法
詳情頁本地化就是客戶端不走網絡請求打開新聞的方案,解決上文中列舉的全部網絡請求相關的優化點。它除了能爲咱們帶來首屏性能的進一步提高以外,因爲它不走網絡請求的特性,也爲咱們解決了複雜網絡環境下頁面劫持致使的詳情頁白頁打不開的問題。同時還爲咱們帶來了無網絡環境下的離線閱讀新聞的能力。npm
因爲咱們的這面是純 JS 渲染的,因此咱們一個最終的詳情頁主要是由新聞數據
和靜態頁面
二者構成的。 鑑於對服務端的依賴很是的少,和大部分的 SPA 頁面同樣,本質上只要在客戶端將咱們的前端頁面提早下載下來就能正常打開了。後端
詳情頁 = 靜態頁面 + 新聞數據
複製代碼
而如何在用戶尚未打開新聞以前客戶端就能把咱們的頁面資源下載下來呢?這裏就不得不提一下咱們的場景,由於在咱們的信息流場景中,用戶永遠是經過流點擊進入到詳情頁中。而在客戶端的流中是須要加載服務端數據的,因此在這個時候其實咱們就能夠告知客戶端讓其提早下載好模板。固然你們不要忘記,除了頁面以外咱們還要有新聞數據,爲了實現純離線化同時也避免新聞數據接口的請求,在列表中還會將每條新聞的詳細數據下發下去,保證必備要素的本地化。跨域
如圖所示,在列表請求的接口中,服務端會將須要緩存的靜態頁面地址以及每條新聞對應的新聞數據所有下發給客戶端,客戶端接收到請求以後會進行模板的下載。緩存
須要的東西下發下去以後剩下的就是客戶端進行渲染了。正常來講除了模板頁面以外,服務端還須要下載其餘相關的靜態資源,而後啓動一個 HTTP 服務將頁面和資源文件進行關聯,關聯以後將數據注入到頁面以後打開頁面。但這對客戶端的要求就很是多了,爲了將客戶端的工做量下降,咱們將全部須要使用的靜態資源經過編譯內聯到 HTML 文件內,客戶端經過字符串拼接的形式將數據注入到頁面的全局變量中。
如圖所示全部靜態資源都被標記了 inline
屬性,咱們的編譯工具在讀取到這個屬性後會將當前資源給內聯到 HTML 中。同時你們注意到該模板不是以 <html>
開頭的,而是有一些截斷。這是爲了給客戶端提供注入數據空間,客戶端經過模板字符串拼接的形式將新聞數據注入到全局變量中最終完成整個新聞頁面的獲取。前端代碼中則直接使用 __INJECT_DATA_FROM_CLIENT_DONT_MODIFY__
全局變量獲取注入的數據。
上面就是一套完整的本地化下發並打開的流程了,總的來說就分爲四步:
可是隻要有資源的分發就會涉及到資源的同步更新問題,咱們的本地化模板也是同樣。在咱們的線上更新的時候如何讓客戶端知曉並觸發更新行爲,也是咱們須要去考慮的問題。實際上你們在前兩張截圖中能夠看到,爲了解決這個問題,咱們是在服務端下發的接口中還增長了一個 version
字段,用來標記當前 HTML 的版本。而當前端進行代碼發佈的時候,咱們的發佈系統會有一個相似 npm 的 postpublish
的鉤子,利用這個鉤子咱們告訴服務端發佈成功更新版本號。最後,當客戶端接收到新的版本號的時候則會從新下載新的模板,完成一次本地模板的更新。
在前端頁面中,Cookie 和 LocalStorage 等大量的特性是和域名相關的,而不巧的是咱們的頁面中都有使用,因此跨域也是咱們須要考慮到的問題。咱們知道,本質上此種方案下客戶端至關於使用 WebView 打開了一個本地頁面,而在 Android 系統中 WebView 打開本地頁面的話有三種方法:
file:///temp.html
的形式打開一個本地文件 URLloadUrl
類型,好的地方在於不須要寫成文件,能夠直接加載頁面字符串,不過此時加載完以後頁面的 URL 是 about:blank
loadData
相似,好的地方在於提供了參數可以設置當前 URL 地址從描述中能夠看到,很明顯最後一種 loadDataWithBaseURL
纔是咱們須要的。客戶端經過這個方法加載,設置當前頁面的 URL 爲真實線上 URL,對於前端來講基本上就和線上環境無異了,本地化和線上 Cookie 和 LocalStorage 的共享都沒有問題。不過這裏須要注意,第一個參數 baseUrl 僅能管住當前頁面,若是頁面作了 history.pushState()
等前進後退操做的話當前頁面地址又會變成 about:blank
,此時須要再設置最後一個參數 historyUrl
才行。
本文給你們講述了實現本地化離線閱讀的方案。除了以上列舉的問題,咱們還碰到了一些細微的問題。例如咱們發如今網絡很差的狀況下客戶端可能會下載模板失敗緩存了不完整的代碼,因此咱們增長了模板的 md5 值一併下發給客戶端用來校驗模板是否下載徹底。又如上文說了模板的更新,實際上內容也會有更新,特別是一些新聞的實時性會有比較高的要求,爲了解決這個問題,咱們會在頁面打開後再次去檢查一下文章的狀態,若是發生變量會切換至線上版本用來規避這個問題。除了這些以外咱們還作了完備的雲控後退方案,能在方案出問題的時候完美回退到普通版本。
其實你們能夠看到,本地化只是咱們在特定場景下決絕性能問題的一種特定思路。它並非使用於全部的場景,因此我在文章開頭也特別強調了一下咱們的應用場景方便你們去理解。可是咱們只要理解這種方案的精髓,我相信在其它的一些特定場合總能發揮它的威力。