原創 panming 百度App技術
前言
效能是每個前端工程師都應該關注的話題,通用的優化手段已有許多文章和實踐,就不再贅述,本篇以百度App個人主頁為例,聊聊針對業務特點進行的一些效能優化實踐。
適用於:傳統意義的優化手段能用的都用了:打包拆包,縮減體積和 HTTP 請求數、CDN和按需載入等,但效能方面仍不太理想。
定義指標,建設報表
優秀方案的制定首先需要準確的資料做支撐。
一般來說,前端效能指標包括DOM ready、First Contentful Paint、白屏、首屏、使用者可操作時間、onload時間等,在實際中需要結合業務本身的特點進行定義,一般通用的指標定義並不能體現使用者在當前業務下的真實體驗。
個人主頁是在百度App客戶端內的web頁面,有 hybrid版(使用file協議直接載入本地HTML和JS、CSS)和web版(開啟一個web URL)兩種不同的開啟方式。
首先,我們瞭解一下個人主頁頁面的結構:
頭部區域展示當前作者的個人資訊,tab區域則是作者創作產生的內容。頁面中所有資料均為非同步獲取。開啟個人主頁需要經歷的過程可簡化成以下幾個:
其中耗時可劃分為端耗時、網路和server耗時、前端渲染耗時三大部分: 根據以上過程,我們制定了定義指標的原則:主頁頁面展現的使用者資料,是頁面內JS 請求資料後的非同步渲染。因此首屏定義為:頭部區域 和 tab列表 第一屏資料渲染完成(使用者真正可見,也即使用者可操作時間)
主頁是由 san 搭建的 SPA 頁面,HTML 上同步的 DOM 並沒有真實內容出現,在首屏使用者資料返回之前,頁面均顯示為 loading 態。因此白屏的定義為:頁面DOM掛載上內容(使用者首次看到頁面不再空白的時間)
由於在端內,iOS 和 安卓 onload 事件觸發的時機不同,iOS上資源載入會阻塞頁面渲染,因此主頁中針對 iOS 進行了調整,使用 rAF 使 iOS 在 JS開始執行時即觸發 onload,而安卓在首屏需要的圖片、jsonp 等資源全部 load 完成後觸發,因此該項指標主要用作輔助作用。
在報表建設的過程中,結合主頁的業務形態(在Android、iOS雙端均有hybrid版和web版兩種)以及指標定義的含義,對整個過程的階段儘可能細化,方式如下:- 將主頁的三種版本進行分組打點:hybrid版,hybrid混合web版,web版。即可避免資料干擾,又可通過控制上線時間,來進行實驗對比
- 新增系統、端內外、起始點作為篩選條件,排除不同的使用條件帶來的資料差異,有助於縮小範圍,定位分析
- 根據主頁的執行流程,將耗時流程細分,進一步定位問題,整個階段細分為:端耗時(從點選到解析頁面head頂部)、同步HTML內各JS引用階段耗時、資料請求耗時、頁面內端能力及各元件生命週期到首屏耗時(耗時部分在實際優化中逐步細化分析)
最終得到每個階段的詳細劃分:
補充說明:-
端到端打點即以使用者進行操作的時刻為起始點來記錄資料,涵蓋了從使用者發起點選操作開始到頁面完全展現之間的客戶端耗時、網路耗時、server耗時、前端各個階段耗時的完整流程打點。
其中,起始時間戳由上一個頁面在使用者操作時進行記錄,並透傳給個人主頁;
前端在每次發起網路請求時記錄當前時間戳,介面的返回值中傳回後端處理介面的耗時,二者的差值即HTTP連線的耗時。
本次採用前端計算上報、平臺展示的方案,打點前端控制靈活且迭代快。 -
端耗時部分前期僅能計算從上一個頁面點選到解析頁面頂部的整體時間。
資料分析,提出優化方向
經過階段一,拿到穩定的資料及頁面各階段耗時,分析並提出解決方案。
圖中效能資料可以看出主要耗時階段:端耗時、引入主profile.js到js內部開始耗時、首屏介面耗時、頁面資料處理和渲染耗時,針對主要耗時階段,優化分為以下幾個方面:- 針對端耗時,前端配合端查詢優化點
- 針對首屏介面耗時,前端聯合server進行介面優化
- 針對JS內部耗時,前端進行自身程式碼優化
著手優化,逐步完善
主頁入口較多,需要相容不同入口情況以及歷史遺留,個人主頁業務基本情況如下:
- 頁面為 SPA 模式,但業務複雜,程式碼總體積較大
- hybrid版 首屏需要的資源由兩個介面返回,且兩個介面存在依賴,在前端序列執行
- web 版頂部使用者資料使用同步資料,依賴的tab介面與hybrid相同
- hybrid版 的特點:
- hybrid版 與 web版 使用不同方式編譯的同一套程式碼,但 web版上第一個介面是同步的,資料隨著 HTML 模板一起返回,而 hybrid 中所有介面均是非同步
- 使用的場景更多
- 採用 file 協議載入 HTML 模板,用 jsonp 的方式請求後端資料介面
按照前端程式碼、工程化、server端、客戶端native框架四個方向分別針對性制定優化方案,以下主要介紹前端可控的程式碼和工程化兩個方面。
前端程式碼
提前觸發iOS onload
- 方法:使用 rAF 巢狀 setTimeout 提前觸發 onload 事件,解決 iOS 資源載入阻塞頁面顯示的情況
- 收益:使用者可見的首屏時間不受 load 阻塞
減少首屏依賴
首屏時間可反映出使用者對頁面速度的感受,首屏所依賴的行為越多,就意味著使用者需要等待的時間越長。因此,在效能優化中需要儘可能地減少在首屏前執行的操作、後置一些非必要的操作,可以在某種程度上提升使用者體驗。
經過一段時間的資料收集分析和程式碼 review,我們發現一些可以改進的地方:
- 在首屏前的一段邏輯裡,JS 初始化一些資料時一次性呼叫了多個 native 提供的方法(端能力),導致端能力執行耗時 80分位值 遠超理論值;
- SPA 頁面的最外層元件 App 在首個介面的資料返回後才進行掛載。對 web版頁面來說,首個介面是同步的,因此 App 在介面後掛載影響不大;但對使用場景更多的 hybrid版 來說,頁面的首屏至少需要兩個介面,而所有介面請求均為非同步,首個介面返回之前足以處理很多頁面必要的邏輯,App 的掛載時機就顯得非常不合適。
- 頁面上埋了很多打點,除pv外其他多是頁面上一些小元件的展現打點,在首屏之前頻繁傳送打點請求擠佔了首屏中圖片的載入時間。
結合以上發現,對程式碼進行了如下調整:
- 調整與native端能力呼叫的執行順序,首屏必要的留下,其他的後置,降低端能力執行耗時
- 優化必要的程式碼資訊(例如:個人主頁從頭用到尾的 runtime)初始化邏輯
- 最外層 App 元件掛載不依賴介面資料,頁面提前進行初始化,介面資料並行請求,非同步渲染
- 調整程式碼執行邏輯,關鍵邏輯移至 store,提前執行
- 頁面內部分打點等邏輯後置,減少頁面掛載執行時間
一波操作下來,獲得了80分位100ms+的收益。
首屏介面合併
上文有提到hybrid首屏需要在前端序列執行兩個非同步網路介面。從統計到的效能資料上看,在呼叫介面到拿到介面資料的過程中,耗時最長的是建立網路連線這個階段,兩個介面合併成一個介面,首屏時間上至少可以節省一次建立網路連線的時間(個人主頁做到了110ms+)。當然,介面合併也需要考慮server端的平響,考慮可能會犧牲的一丟丟白屏時間。
首屏介面前置
作為一個標準的 SPA 頁面,個人主頁頁面上幾乎所有的邏輯都是在公共 js 載入完成之後才開始執行,但 js 載入需要時間,尤其是首次載入、本地還沒有快取時。
hybrid 版本沒有同步介面,只能在 js 載入完成之後才發出首屏的第一個請求,因此 hybrid 的版本在這裡還存在可優化空間。
已知現在主流瀏覽器可並行處理的請求通常預設在4~6個,在載入js時去拿首屏需要的資料(jsonp),序列變並行,節省下來一份二者重疊的時間。
首屏介面前置就做了這麼一些事:
hybrid 打包時內聯了一個體積儘可能小的極簡程式碼包,去取首屏第一個介面的資料,完成後存入全域性變數並以事件的形式派發出來。由於 iOS 部分場景中首次請求建立網路連線的耗時較長,順便使用 native 端能力代替 jsonp,首屏介面前置中iOS收益在260ms+,安卓60ms左右。
工程化
工程化上進行的優化主要是在打包上下功夫,打包影響載入 JS、CSS 等資源的 http 耗時,在相同的條件下,包體積越小、請求次數越少,資源載入速度就越快。
打包和拆包
JS 和 CSS 資源打包合併,但需要考慮打包檔案過大,單個請求耗時太長,需要結合業務場景合理拆分程式碼包。 主流瀏覽器可並行處理的請求通常預設在 4~6個,可合理拆分資源包,利用並行請求縮減整體的響應時間。 通過合理劃分包來最大程度上利用瀏覽器快取。鎖版本、保持每個小 bundle 未發生改變時雜湊值穩定,較大的 JS、CSS 和圖片等會被直接寫進硬碟快取。例如,個人主頁根據程式碼的修改頻率把 js 包拆成體積差不多大小的三個,其中 vendors 是各種 npm 依賴,版本穩定,通常不會發生改變。每次上線後使用者瀏覽器只需要從 CDN 上請求另外兩個程式碼包,vendors 則使用上次還未過期的本地快取。
現代模式(modern mode)
通常,開發時我們使用 ES6(ES2015+) 來編寫程式碼,ES6 的新特性可以讓開發工作更便捷迅速但打包時需要用 Babel 進行轉換來讓我們的程式碼能執行在不支援 ES6 的瀏覽器上。轉換後的程式碼會加入 polyfill,最直觀的感受就是程式碼包體積增大。modern mode 在支援原生 ES6 的瀏覽器中,js會通過 載入 ES模組 的<script type="module"> 載入,而在不支援的瀏覽器中使用 <script nomodule> 來載入 babel 編譯後的版本,並且支援ES模組的瀏覽器會忽略這種寫法。在支援 ES6 的瀏覽器上使用 ES6 的版本,程式碼 bundle 體積更小、解析和執行的速度更快,何樂而不為呢? 從個人主頁收集到的資料顯示,目前已有 75% 的場景支援 modern mode,帶來的效能收益也是非常可觀:
白屏(ms) | 首屏(ms) | |
---|---|---|
實驗組 | 1055 | 1913 |
對照組 | 1136 | 2081 |
收益 | 81 | 168 |
優化效果總結
優化後JS初始化耗時減少,首屏資料jsonp請求使用內聯的極簡程式碼包在頁面準備完畢前就發出,在jsonp得到返回值前並行載入頁面需要的其他CSS和JS資源;App掛載和頁面runtime初始化的時間提前,首屏資料回來後可以立馬處理並渲染資料,而不被其他的一些操作佔用寶貴的時間;打點等請求後置,首屏完畢前讓JS專注於資料和渲染,同時騰出頻寬載入圖片等使用者可見的資源。優化後的流程可用下圖表示:
優化後資料分析 雙端首屏時間均大幅度減小,首屏請求(兩圖中橙色部分)得益於 server 同學的介面效能優化,單單是平響就降低了80ms+;而安卓上得益於端同學的 hybrid 框架優化,使得 hybrid 頁面和本地js資源載入速度更快,效果更是顯著。
總結
工欲善其事,必先利其器。在著手進行優化前,有大量的時間花在了選取資料參考點、收集資料上,通過反覆的 code review、實驗、業務邏輯推敲來確保每個關鍵指標反映的都是真實可信的資料。本文僅僅提供一種從資料著手的優化點分析方法,列舉的優化方法與實際業務密不可分,並不具有太強的普適性,希望能給大家在解決瓶頸的道路上帶來一點不一樣的思路。