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

公子發表於2019-07-29

開篇之前先介紹一下場景。資訊流是一個基於使用者興趣使用演算法將使用者感興趣的新聞內容推薦給使用者的一種業務。這種業務帶有非常特色的場景就是使用者有一個“永遠”都刷不完的推薦流列表,點選列表中的新聞之後可以跳轉到其詳情頁中檢視新聞的正文內容。列表一般都是由客戶端原生去實現的,而詳情頁這塊由於新聞內容結構的複雜性,一般還是會使用 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 值一併下發給客戶端用來校驗模板是否下載完全。又如上文說了模板的更新,實際上內容也會有更新,特別是一些新聞的實時性會有比較高的要求,為了解決這個問題,我們會在頁面開啟後再次去檢查一下文章的狀態,如果發生變數會切換至線上版本用來規避這個問題。除了這些之外我們還做了完備的雲控後退方案,能在方案出問題的時候完美回退到普通版本。

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

相關文章