不到 0.3s 完成渲染!360 信息流正文“閃開”優化實踐

開篇之前先介紹一下場景。信息流是一個基於用戶興趣使用算法將用戶感興趣的新聞內容推薦給用戶的一種業務。這種業務帶有非常特色的場景就是用戶有一個“永遠”都刷不完的推薦流列表,點擊列表中的新聞之後可以跳轉到其詳情頁中查看新聞的正文內容。列表一般都是由客戶端原生去實現的,而詳情頁這塊由於新聞內容結構的複雜性,一般還是會使用 h5 來實現。這樣就對我們 h5 的性能提出了要求,我們必須在用戶切換的時候將切換的白屏時間儘量減少,這樣才能提高用戶的閱讀體驗。

本文就將爲大家講述一下我們是如何實現性能優化達到“閃開”的效果的。我們可以先看看效果
https://v.qq.com/x/page/j0900...,下圖左邊是正常版本,而右邊的是優化後的版本。對比之下可以發現即使我已經悄咪咪的先點擊左邊的手機,同一篇新聞右邊的打開速度明顯比左邊的要快很多。接下來就讓我們看看這個是如何做到的吧!

目前現狀

衆所周知,網頁中內容渲染往往根據渲染方式可以分爲後渲染和前端渲染兩種方式,最近幾年由前端渲染又演化出了同構渲染,也就是大家經常說的 SSR。這幾種渲染方式的主要優缺點大概整理了主要有如下幾個方面。

  1. 後端渲染:

    • 優勢:服務端直出首屏性能好,SEO好
    • 劣勢:交互邏輯複雜需要兩端維護結構
  2. 前端渲染:

    • 優勢:前端交互易維護,數據渲染分離
    • 劣勢:首屏性能問題以及 SEO 問題
  3. 同構渲染:

    • 優勢:首屏性能好,SEO 好,一份代碼多端運行
    • 劣勢:代碼維護成本,服務器性能和維護成本增加

當然本篇文章不是來講各種渲染方式的優缺點的,主要是說因爲種種原因我們的項目最後使用了前端 JS 渲染的方式。而 JS 渲染帶來的性能問題主要是由於數據接口請求返回以及前端 JS 資源獲取所帶來的網絡問題。爲了解決這兩個問題,一方面我們採用了服務端將數據注入到頁面全局變量中的方式避免了數據請求,另一方面我們使用了 localStorage 緩存的方式將前端資源做了 LS 緩存避免了二次打開之後的前端資源請求,從而提高了前端渲染的首屏性能。

思考優化方案

雖然我們避免了前端渲染的一些問題對首屏的性能做了優化,但還遠遠不夠。那目前還有哪些點可以進行優化呢?簡單的整理了下可以有如下兩個方面:

  • 首次進入以及線上代碼有更新之後還是需要下載前端資源
  • 服務端頁面的 ttfb 相應還有優化的空間
  • 客戶端 WebView 打開的速度和性能還有優化的空間

從上面兩個優化點我們可以看到所有的優化還是網絡的優化,主要還是在移動端網絡對性能的影響是遠遠大於其他方面的。那麼是否有什麼方案能夠讓我們免去這些網絡請求呢,最終我們給的答案就是詳情頁本地化。通過本地化方案,我們將平均 820ms 的首屏渲染時間優化到了 260ms,整整提高了三倍多

詳情頁本地化就是客戶端不走網絡請求打開新聞的方案,解決上文中列舉的所有網絡請求相關的優化點。它除了能爲我們帶來首屏性能的進一步提升之外,由於它不走網絡請求的特性,也爲我們解決了複雜網絡環境下頁面劫持導致的詳情頁白頁打不開的問題。同時還爲我們帶來了無網絡環境下的離線閱讀新聞的能力。

本地化實現

由於我們的這面是純 JS 渲染的,所以我們一個最終的詳情頁主要是由新聞數據靜態頁面兩者構成的。
鑑於對服務端的依賴非常的少,和大部分的 SPA 頁面一樣,本質上只要在客戶端將我們的前端頁面提前下載下來就能正常打開了。

詳情頁 = 靜態頁面 + 新聞數據

數據預下發

而如何在用戶還沒有打開新聞之前客戶端就能把我們的頁面資源下載下來呢?這裏就不得不提一下我們的場景,因爲在我們的信息流場景中,用戶永遠是通過流點擊進入到詳情頁中。而在客戶端的流中是需要加載服務端數據的,所以在這個時候其實我們就可以告知客戶端讓其提前下載好模板。當然大家不要忘記,除了頁面之外我們還要有新聞數據,爲了實現純離線化同時也避免新聞數據接口的請求,在列表中還會將每條新聞的詳細數據下發下去,保證必備要素的本地化。

如圖所示,在列表請求的接口中,服務端會將需要緩存的靜態頁面地址以及每條新聞對應的新聞數據全部下發給客戶端,客戶端接收到請求之後會進行模板的下載。

客戶端行爲

需要的東西下發下去之後剩下的就是客戶端進行渲染了。正常來說除了模板頁面之外,服務端還需要下載其他相關的靜態資源,然後啓動一個 HTTP 服務將頁面和資源文件進行關聯,關聯之後將數據注入到頁面之後打開頁面。但這對客戶端的要求就非常多了,爲了將客戶端的工作量降低,我們將所有需要使用的靜態資源通過編譯內聯到 HTML 文件內,客戶端通過字符串拼接的形式將數據注入到頁面的全局變量中。

如圖所示所有靜態資源都被標記了 inline 屬性,我們的編譯工具在讀取到這個屬性後會將當前資源給內聯到 HTML 中。同時大家注意到該模板不是以 <html> 開頭的,而是有一些截斷。這是爲了給客戶端提供注入數據空間,客戶端通過模板字符串拼接的形式將新聞數據注入到全局變量中最終完成整個新聞頁面的獲取。前端代碼中則直接使用 __INJECT_DATA_FROM_CLIENT_DONT_MODIFY__ 全局變量獲取注入的數據。

頁面的更新

上面就是一套完整的本地化下發並打開的流程了,總的來講就分爲四步:

  1. 前端將頁面處理成真·單頁應用
  2. 服務端在列表時將數據和本地化模板下載地址通過接口下發給客戶端
  3. 客戶端獲取到模板下載地址後進行下載
  4. 當用戶打開新聞的時候客戶端將數據和模板進行拼接打開即可

但是只要有資源的分發就會涉及到資源的同步更新問題,我們的本地化模板也是一樣。在我們的線上更新的時候如何讓客戶端知曉並觸發更新行爲,也是我們需要去考慮的問題。實際上大家在前兩張截圖中可以看到,爲了解決這個問題,我們是在服務端下發的接口中還增加了一個 version 字段,用來標記當前 HTML 的版本。而當前端進行代碼發佈的時候,我們的發佈系統會有一個類似 npm 的 postpublish 的鉤子,利用這個鉤子我們告訴服務端發佈成功更新版本號。最後,當客戶端接收到新的版本號的時候則會重新下載新的模板,完成一次本地模板的更新。

跨域問題

在前端頁面中,Cookie 和 LocalStorage 等大量的特性是和域名相關的,而不巧的是我們的頁面中都有使用,所以跨域也是我們需要考慮到的問題。我們知道,本質上此種方案下客戶端相當於使用 WebView 打開了一個本地頁面,而在 Android 系統中 WebView 打開本地頁面的話有三種方法:

  • loadUrl:本質上使用 file:///temp.html 的形式打開一個本地文件 URL
  • loadData:和 loadUrl 類型,好的地方在於不需要寫成文件,可以直接加載頁面字符串,不過此時加載完之後頁面的 URL 是 about:blank
  • loadDataWithBaseURL:和 loadData 類似,好的地方在於提供了參數能夠設置當前 URL 地址

從描述中可以看到,很明顯最後一種 loadDataWithBaseURL 纔是我們需要的。客戶端通過這個方法加載,設置當前頁面的 URL 爲真實線上 URL,對於前端來說基本上就和線上環境無異了,本地化和線上 Cookie 和 LocalStorage 的共享都沒有問題。不過這裏需要注意,第一個參數 baseUrl 僅能管住當前頁面,如果頁面做了 history.pushState() 等前進後退操作的話當前頁面地址又會變成 about:blank,此時需要再設置最後一個參數 historyUrl 才行。

後記

本文給大家講述了實現本地化離線閱讀的方案。除了以上列舉的問題,我們還碰到了一些細微的問題。例如我們發現在網絡不好的情況下客戶端可能會下載模板失敗緩存了不完整的代碼,所以我們增加了模板的 md5 值一併下發給客戶端用來校驗模板是否下載完全。又如上文說了模板的更新,實際上內容也會有更新,特別是一些新聞的實時性會有比較高的要求,爲了解決這個問題,我們會在頁面打開後再次去檢查一下文章的狀態,如果發生變量會切換至線上版本用來規避這個問題。除了這些之外我們還做了完備的雲控後退方案,能在方案出問題的時候完美回退到普通版本。

其實大家可以看到,本地化只是我們在特定場景下決絕性能問題的一種特定思路。它並不是使用於所有的場景,所以我在文章開頭也特別強調了一下我們的應用場景方便大家去理解。但是我們只要理解這種方案的精髓,我相信在其它的一些特定場合總能發揮它的威力。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章