今日頭條品質優化:圖文詳情頁秒開實踐

背景

作爲一個內容類應用,看新聞讀資訊一直是頭條用戶的核心需求,頁面的打開速度直接關係到用戶使用頭條的核心體驗,在頭條中,爲了更多的承載足夠豐富的樣式和邏輯下保持多端體驗的統一,詳情頁的內容我們是通過 WebView 來承載的,但 WebView 本身的性能相比 Native 來說比較差,因此,今日頭條技術團隊一直致力於優化詳情頁的加載速度。

經過不斷的優化,目前今日頭條中詳情頁在線上的打開體驗,從肉眼上基本已經感知不到加載過程。在接下來這篇文章裏,我們會逐步拆解和介紹我們對詳情頁加載優化的思路和實踐。

先讓我們來看看優化前後的效果吧~

今日頭條詳情頁加載體驗優化前

今日頭條詳情頁加載體驗優化後

數據建立

性能

當我們開始着手優化頁面加載速度之前,我們需要明確一個問題,怎樣纔是用戶真正體驗到的頁面加載時間。

首先我們可以看下面這個公式:

頁面加載時間 = 頁面加載完成時間 - 頁面開始加載時間

頁面開始加載時間很好確定,當用戶點擊了 Feed 上的卡片,我們就可以認爲頁面開始加載了。

問題是怎麼定義頁面加載完成了呢?從客戶端的角度上看,無論是 iOS 還是 Android,WebView 都提供了一個 loadFinsih 的回調,但在實際應用中我們發現,loadFinish 回調並不能反應用戶的真實體驗。

一般來說,WebView 渲染需要經過下面幾個步驟

  1. 解析 HTML 文件
  2. 加載 JavaScript 和 CSS 文件
  3. 解析並執行 JavaScript
  4. 構建 DOM 結構
  5. 加載圖片等資源
  6. 頁面加載完畢

而 loadFinish 實際上是在頁面加載完畢階段,而 DOM 構建完成時頁面結構就已經基本渲染完成,所以從用戶真實體驗的角度出發,我們以 DOM 結構構建完成(即 domReady)的時間點作爲頁面加載完成時間點。

白屏

在詳情頁瀏覽過程中,除了頁面加載速度之外,還有一個特別影響用戶體驗的問題,就是頁面的白屏,也是早期的時候用戶反饋比較多的問題,但有很多場景都可能導致詳情頁發生白屏,比如說網絡異常,WebView 異常等等,需要從用戶體驗的角度出發去檢測用戶發生白屏的情況。

目前可以想到最直觀的方案就是對 WebView 進行截圖,遍歷截圖的像素點的顏色值,如果非白屏顏色的顏色點超過一定的閾值,就可以認爲不是白屏,目前需要考慮的是這個方案的性能問題和檢測時機。

iOS 中提供了 WebView 快照的接口獲取當前 WebView 渲染的內容,底層採用異步回調的實現方式,API 耗時 10ms 左右,用戶基本無感知。

- (void)takeSnapshotWithConfiguration:(nullable WKSnapshotConfiguration *)snapshotConfiguration completionHandler:(void (^)(UIImage * _Nullable snapshotImage, NSError * _Nullable error))completionHandler API_AVAILABLE(ios(11.0));

Android 中系統提供的獲取視圖內容的接口爲 getDrawingCache,API 耗時在 40ms 左右,性能損耗也不是特別大。

除了截圖的性能損耗,像素點檢測也是白屏檢測中比較耗時的場景,經過實驗,我們把 WebView 截圖的圖片進行縮小到原圖的 1/6,遍歷檢測圖片的像素點,當非白色的像素點大於 5% 的時候我們就認爲是非白屏的情況,可以相對高效檢測準確得出詳情頁是否發生了白屏。

指標建立

確定好口徑之後,我們還有需要明確的一個問題是,什麼指標可以反映用戶刷頭條時的真實體驗。

最早的時候,我們用的是詳情頁頁面的頁面平均加載時長,也就是頁面加載時長的總和/頁面 pv,在開始的時候這個指標也的確可以明確我們的加載速度。

後來隨着詳情頁的加載優化逐漸的深入,會發現平均加載時長雖然也可以反映詳情頁加載速度,但是因爲詳情頁的 pv 比較高,如果使用平均加載速度化很多用戶體驗問題就被平均掉了,並不能反映用戶的真實情況,後面我們又調整了口徑,將指標調整爲所有用戶進入詳情頁的 80 分位值,比如說,假如頭條詳情頁加載速度 80 分位值是 1 秒,那麼就說明 80% 的情況下用戶進入詳情頁都能在 1s 內加載完成,當然經過我們的不斷優化,詳情頁加載的 80分位值已經能夠達到 0.3s 以內,也就是說,80% 的情況下用戶都能夠在 0.3s 內完成頁面加載。

80分位優化數據對比

再後來我們又發現,在頭條詳情頁的量級下面,即使是 80 分位的數據也不能反應許多長尾用戶的真實情況,也爲了更極致的追求詳情頁的加載性能,我們最後將詳情頁的性能口徑調整到 95 分位。到目前在我們的努力下,今日頭條詳情頁的加載速度 95 分位也優化了將近 80% 。

我們究竟做了什麼呢,接下來會慢慢介紹一下。

模板優化

模板拆分

如前所述,圖文詳情頁是通過 WebView 來承載的,而 WebView 承載頁面最簡單的做法就是直接通過 URL 去加載一個線上頁面。那麼先來一道簡單的面試題,當用戶從瀏覽器輸入一個 URL 到頁面展現發生了什麼呢?

之前已經介紹過頁面的渲染流程了,現在我們再簡單看看用戶從點擊到看到頁面內容需要經歷如下幾個階段:

WebView 加載流程

可以看到,通過線上頁面加載用戶每次進入詳情頁都要通過多次網絡加載,極容易受網絡波動的影響,這種情況下,也無法保證頁面加載的時長和成功率,極大的影響了用戶體驗。

於是在頭條中,我們將新聞中標題和正文內容進行拆分,把頭條詳情頁的公共樣式 CSS 和 邏輯 JS 都抽離出來,形成一個獨立而完備的詳情頁模板,這樣我們就可以把模板直接內置在客戶端中。

同時我們會與前端約定好的 JS 腳本,通過接口將正文內容數據注入頁面完成詳情頁的頁面展示,通過該這種方式我們可以將接口放到客戶端上進行請求。

這樣用戶進入詳情頁的時候只需要本地加載模板,而且加載模板的時候也可以同時並行請求詳情頁數據,再將數據注入進模板中。

那麼用戶點擊到看到頁面內容只需要經歷下面的階段:

模板拆分

如上圖所示,我們只需要通過一次網絡加載就可以完成頁面渲染。

還能不能更快一點呢?當然能!

爲了提高頁面的加載速度,客戶端通過一定的策略去預加載新聞數據,這樣在理想狀態下用戶進入頁面時看到頁面時就可以直接使用緩存的數據,用戶在看新聞的時候可以實現完全離線化,避免受到網絡的影響。

本地加載

模板預熱

完全脫離了網絡加載之後,還能再快一點呢?當然還是可以的!

當全流程離線化之後,頁面加載的瓶頸就變成了本地模板的加載時間,所以我們接下來要做的就是優化模板加載時間。

對於模板來說,我們做了兩件事情

  1. 模板合併,正常來說,WebView 需要在加載完主 HTML 之後再去加載 HTML 中的 JS 和 CSS,需要多次 IO 操作,於是我們將 JS 和 CSS 還有一些圖片都內聯到一個文件中,這樣,加載模板時就只需要一次 IO 操作,也大大減少因爲 IO 加載衝突導致模板加載失敗問題
  2. 模板簡化,我們將部分非必須的腳本異步化拉取,精簡不必要的樣式和 JS 代碼,將模板大小壓縮了 20% 以上

通過上面優化,我們就已經將模板加載時間大大優化了,但是還能不能更給力呢?還是可以的。

對於客戶端來說,當模板跟數據分離之後,由於每次用戶點擊的時候加載的都是同一個模板,所以實際上,我們並不需要在用戶進入頁面的時候纔去創建 WebView 以及加載模板,我們只需要在合適的時機在後臺創建 WebView,並且提前預熱加載模板,當用戶點擊進入頁面的時候就能使用已經加載好模板的 WebView,直接將詳情頁的內容數據通過 JS 注入到頁面中,前端收到數據後進行頁面渲染即可。

此時用戶進入詳情頁實際就不再需要重新加載模板了,路徑就變成了:

模板預熱

可以看下,通過本地測試的模板預熱和數據預取的優化效果,還是比較明顯的,基本上已經達到了上面的截圖中的驗證效果。

本地測試數據

模板複用

當我們拆分完模板和數據之後,數據上優化已經比較明顯,但我們說過,除了驗證數據,我們還需要看線上用戶的真實體驗數據,從 95 分位上看實際數據優化卻不是很明顯,所以我們從數據上觀察,用戶預熱模板的命中率只有 53%,還有進一步的提升空間。

模板預熱率

爲了儘可能的提高頁面的加載速度,我們希望用戶每次進入詳情頁的時候都能夠使用預熱好模板的 WebView,一般情況下,我們都會使用模板預創建池的手段來優化用戶進入詳情頁時的預熱模板命中率。

但其實在很多情況下,WebView 的創建是一個性能開銷比較大的操作,如果我們使用預創建池的方案,那麼就會在後臺頻繁創建 WebView,這樣對用戶在 Feed 場景的瀏覽體驗也會有一定的影響。

而且假如用戶頻繁且快速進出詳情頁時,實際場景中用戶也很容易遇到無法命中預熱模板的場景。

這個時候爲了優化用戶的體驗,如前文所述,我們每次使用的時候都是同一個模板,所以我們使用完當前 WebView 之後,只需要在用戶退出頁面的時候把正文數據清空,這樣進入下一個頁面的時候就能夠繼續複用這個 WebView 重新注入數據即可。

通過這個手段,我們既避免了頻繁在後臺預創建 WebView 對用戶刷 Feed 體驗的影響,把用戶進入頁面時候的預熱模板命中率從 53% 提升到 92%,優化了用戶體驗。

預熱模板命中率

網絡優化

說完我們在模板 WebView 方面的優化之後,再介紹一下我們在內容請求上的優化。

CDN 加速

由於頭條詳情頁請求有以下特點

  1. 流量大,之前說過,看新聞作爲用戶在頭條的核心場景,每天都有上億用戶在使用頭條,詳情頁的數據流量十分大。
  2. 數據屬性基本不變,在詳情頁的請求中,很多熱點文章是重複渲染計算的,正文、標題、作者信息、圖片控制以及一些樣式和業務邏輯渲染是基本不變的,這部分重複計算耗費了帶寬、服務器資源,是比較沒有必要的。
  3. 用戶分佈廣,網絡狀況難以保證,頭條的用戶量很大,覆蓋了各種運營商網絡和網絡狀態,網絡質量無法得到很好的保證。而 CDN 能夠將數據緩存在各地的邊緣節點,用戶就近接入了邊緣節點,避免在網絡質量無法保證的公網上長時間傳輸,從而提高了響應速度和響應的成功率。
  4. 接口數據大,由於正文數據的存在,接口返回的數據常常會很大,如果每一次都實時返回,對網絡的壓力會比較大,可能會把帶寬打滿而影響其他服務

所以我們將詳情頁內容數據分爲靜態和動態兩部分,將正文內容、標題、作者欄等用戶主要消費的又基本不變的內容託管到了 CDN 上。

CDN 的全稱是 Content Delivery Network,即內容分發網絡。其目的是通過在現有的Internet中增加一層新的網絡架構,將網站的內容發佈到最接近用戶的網絡“邊緣”,使用戶可以就近取得所需的內容,提高用戶訪問網站的響應速度。CDN 有別於鏡像,因爲它比鏡像更智能,或者可以做這樣一個比喻:CDN = 更智能的鏡像 + 緩存 + 流量導流。因而,CDN 可以明顯提高Internet網絡中信息流動的效率。從技術上全面解決由於網絡帶寬小、用戶訪問量大、網點分佈不均等問題,提高用戶訪問網站的響應速度。

託管到 CDN 之後,全國各地的用戶可以直接從最佳節點就獲取到詳情頁數據,也大大節省了帶寬成本。

容災

1. 多域名備份

爲了防止某個 CDN 出現故障,導致服務雪崩,服務端會下發多個 CDN 鏈接,當用戶訪問當前 CDN 節點的出異常時,可以快速自動切換到下個 CDN 節點。

2. 快速超時

一般的超時策略,客戶端在請求時,會遍歷請求 CDN 1、2、3。如果這些 CDN 都請求失敗,則整個網絡請求算作失敗。

但這個方案的問題是,假設請求 CDN 的超時時間是 15s。如果 CDN 1 出現故障,則需要等待 15s 才能切換到 CDN 2上,這對於詳情頁的加載時間來說是不可接受,如果用戶網絡突然變差,則需要等待 45s 才能返回失敗展示錯誤頁。

基於此我們設計了詳情頁請求的快速動態超時策略

  • 單次請求 CDN 的超時時間,根據上次成功請求 CDN 的值計算,因子 1.5(z值)。且最小爲 1s(x值),最大爲 4s(y值)。超過這一時間不取消,直接請求下個 CDN。
  • 單次請求 CDN 有一個硬性超時時間 4s(w值,w需>=y),超過這一時間請求取消。n 個 CDN 的請求全部取消後反饋用戶失敗。

幾個 case

  • 第1個 CDN 突然掛掉(假設上次成功請求的耗時爲a)下一次請求:第一個 CDN 很快超時(a * 1.5);開始請求第二個 CDN(超時時間爲 a * 1.5,但實際上 b 秒就會返回請求)。用戶本次等待時間爲 a * 1.5 + b
    下兩次請求:第一個 CDN 很快超時(b * 1.5);開始請求第二個 CDN(超時時間爲 b * 1.5,但實際 c 秒就會返回請求)。用戶本次等待時間爲 b * 1.5 + c

  • 用戶突然進入了一個網絡很差的環境(假設上次成功請求的耗時爲a)下一次請求:第一個CDN很快超時(a * 1.5);開始請求第二個 CDN(a * 1.5)也超時;開始請求第三個 CDN(a * 1.5)。最後一個請求會在 a * 3 + w 後返回失敗(這個值會在12s以內)。

可以看到,通過多域名備份和快速超時的策略,即使用戶在網絡或者服務異常的情況下,也能快速恢復或者讓用戶能感知到自身網絡問題。

渲染優化

當我們在模板層和網絡層優化到極致的時候,限制我們的就是 WebView 的渲染速度了!

服務端預渲染

正常來講,正常的內容數據可能是類似 JSON 等數據,客戶端獲取到數據之後,將數據注入給前端,前端還需要將 JSON 數據跟模板進行組裝,拼上 HTML 標籤等模板了之後再呈現到 WebView 渲染,導致前端渲染上耗時也比較久。

爲了提高用戶的首屏效率,我們在服務端就會把所有的詳情頁正文的 HTML 數據組裝好,通過將服務端直出內容注入到頁面中時,可以直接給 WebView 進行渲染,對於其他動態下發的內容(比如相關搜索),前端再進行二次異步處理,提升用戶效率。

客戶端渲染

一般來說,我們正文中所有內容都是通過 WebView 渲染,經過上述的優化之後,文章的文字部分渲染效率已經很高了,但是實際場景中,很多文章會包含比較多的圖片和視頻場景。

在實際場景中,WebView 渲染非文字內容會存在以下問題:

  1. 相比於文字內容,非文字內容比如說圖片和視頻類資源的渲染對於 WebView 來說渲染效率比較差
  2. 在詳情頁中文章有大量圖片的場景,對於 WebView 的渲染內存佔用和滑動體驗也有問題
  3. 最後,如果用戶多次打開同一篇文章,這篇文章中的圖片也會存在多次加載的問題,無法與客戶端進行緩存共享,對用戶的流量也是一種浪費。

所以在詳情頁中,我們會將圖片和視頻等非文字內容通過原生組件的方式放在客戶端進行渲染,既可以提高渲染效率,也可以減少不必要的流量消耗。

原生化渲染還有一個好處,圖片越來越成爲文章體驗的重要部分,對於多圖文章,我們在 Feed 頁面也可以智能加載詳情頁需要的圖片,增加用戶的文章首屏體驗。

白屏優化

講完了性能優化,最後再分享一下我們對詳情頁白屏率的一些優化,其實很多用戶反饋白屏問題大部分都可能是由於網絡等問題導致頁面加載時間過長,導致用戶從體驗上觀感是白屏了,這部分通過上面分享的性能優化手段已經能夠解決,所以下面只是簡單介紹下一些非網絡原因的白屏問題。

我們通過白屏檢測和上報之後的數據分析之後發現,非網絡原因導致的詳情頁的白屏問題大體是 WebView 加載的問題。

在 iOS 中,我們使用的是系統提供的 WKWebView,WKWebView 是運行在一個獨立進程中的組件,所以當 WKWebView 上佔用內存過大時,WKWebView 所在的 WebContent Process 會被系統 kill 掉,反映在用戶體驗上就是發生了白屏。

根據網上的做法,我們可以在 WKWebView 提供的回調 webViewWebContentProcessDidTerminate 函數中通過 reload 方法重新加載當前頁面恢復,但是這種情況只適用於通過 loadRequest 加載的請求,在詳情頁中,由於使用了模板化的 WebView 中,重新 reload 只能重新 reload 模板,並不能正常恢復整個詳情頁,需要客戶端重新加載模板之後再重新注入數據。

另外由於我們有預熱模板的邏輯,所以可能在進入詳情頁的時候使用的 WKWebView 就已經崩潰,在調用 JS 注入數據時會直接返回失敗,失敗時,我們會嘗試重新加載模板。但後來實際操作中發現一個問題,如果直接調用數據注入的方法,等待系統 WebView 返回失敗的回調耗時比較久,所以後續也調整了數據注入的接口,我們提前在注入的腳本中判斷是否存在數據注入的接口,如果不存在,就說明模板存在問題,直接重試即可。

而在 Android 中,我們採用的是自研內核 WebView,也會遇到一些奇奇怪怪的坑。

  1. 多線程讀模板文件問題,WebView 在運行中會讀取的文件模板,如果此時另外一個線程同時更新模板文件時,就出現了模板加載問題,所以需要保證模板加載的原子性
  2. Render 卡死問題,內核是一個比較複雜的邏輯,內部渲染極少數情況也會出現 Render 卡死問題,但是在詳情頁整體用戶的量級下,即使只有十萬分之一的可能,對用戶來說也是一個比較大的問題,此時我們會從業務上做白屏監控進行重試

當然不管是 iOS 和 Android, WebView 加載的邏輯都比較複雜,有時候怎麼重試也無法成功,這個時候我們會直接降級到加載線上的詳情頁,優先保證用戶的體驗。

總結

限於篇幅原因,我們還做了很多其他事情,包括請求精簡,push 文章預拉取,數據注入的方式優化等等,也做了很多其他的方向的探索,這裏不做展開,希望有機會能再分享給大家。

最後總結一下我們在優化詳情頁打開速度之後的一些想法

  • 數據很重要 ,我們在優化加載速度之前做的第一件事情其實是建立了一個詳情頁的數據看板,只有通過數據我們才能真正瞭解目前線上用戶的現狀,從真實用戶的體驗中找到瓶頸和優化點。
  • 用戶體驗優先 ,優化方案有很多,除了加載速度之外,還需要從整體應用體驗出發,選擇對用戶最佳的方案
  • 追求極致 ,其實最開始的優化是比較簡單的,但是越到後面越難,需要一點點摳細節,才能達到極致的用戶體驗

本文轉載自公衆號字節跳動技術團隊(ID:)。

原文鏈接

今日頭條品質優化:圖文詳情頁秒開實踐

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