乾貨 | 從47%到80%,攜程酒店APP流暢度提升實踐

語言: CN / TW / HK

一、背景

APP效能提升一直是研發團隊永恆的主題。在進行APP效能優化實踐中,除了效能技術方案本身外,還會面臨兩方面問題:第一,APP的效能優化,不具有持續性,往往經過一段時間優化實踐,效果明顯,但是隨著後續需求迭代和程式碼變更,APP效能很難維持在一個較好的水平上;第二,APP效能改善提升,缺乏一套科學量化手段進行衡量。

引⽤管理學⼤師彼得•德魯克的⼀句話:If you can’t measure it, you can’t improve it,如果你⽆法度量它,你就⽆法改進它。基於此,攜程酒店前端APP團隊進行了深入思考和探索,希望通過量化,治理,監控三方面手段,持續改善APP效能和使用者體驗。

二、流暢度指標定義

流暢度,簡單說就是度量使用者使用APP體驗的一部分,它是使用者快速、無阻礙使用APP的一項體驗指標。主要包括三方面內容:穩、快、質。穩的含義是使用者在開啟具體一個頁面時,沒有出現白屏、崩潰、閃動等。快的含義是頁面開啟很快,使用者在頁面進行互動時,操作流暢自然。質的含義,是在瀏覽頁面時,沒有無故的彈窗攔截,打斷使用者的操作。如下圖所示:

基於以上理論基礎,APP中白屏,崩潰閃退,載入慢,卡頓,閃動,報錯,都是使用者在感知層面形成不流暢的因素。於是我們提出了流暢率量化指標,把使用者頁面PV以及使用者在頁面觸發的二次載入次數之和,定義為流暢率的分母,也就是樣本總量,如下公式:

樣本量 = 頁面pv+二次載入數

把頁面慢載入/頁面卡頓/圖片/影片慢載入PV去重後數量,加上頁面出現的崩潰,滑動卡頓,圖片/影片載入失敗,全域性彈窗報錯,輸入失焦,按鈕點選無效,二次載入失敗,二次載入慢等異常情況之和定義為不流暢因子數。那麼流暢率的公式定義為:

流暢率 = (樣本量-不流暢因子數)/ 樣本量

2.1 頁面可互動載入時長

頁面可互動載入時長,是頁面渲染繪製時間疊加網路服務的請求響應時間,可以簡單用下面公式表示:

頁面可互動載入時長(TTI)= 頁面本地渲染時長+服務網路載入時長

2.2 頁面可互動載入時長採集原理

在我們的核心頁面中,都包含了Text控制元件,可以通過掃描頁面中特定區域內的文字來確定使用者可互動時間。我們的技術棧大體上分為Flutter和Ctrip React Native,以下分別介紹載入時長採集原理。

2.2.1 Flutter頁面可互動載入時長採集原理

在Flutter中,最終的UI樹其實是由一個個獨立的Element節點構成。UI從建立到渲染的大體流程如下:

根據Widget生成Element,然後建立相應的RenderObject並關聯到Element.renderObject屬性上,最後再通過RenderObject來完成佈局排列和繪製。如下圖如所示:

所以可以從根節點開始遍歷Element,直到找到掃描視窗內的Text元件且元件的內容不為空,即可判定頁面TTI檢測成功,Flutter提供如下介面支援Element遍歷:

voidvisitChildElements(ElementVisitor visitor)

2.2.2 Ctrip React Native頁面可互動載入時長採集原理

我們知道,ReactNative最終是由Native元件來渲染的,在iOS/Android中可通過從根View從View樹中遞迴查詢Text文字控制元件,來獲取頁面內文內的內容,去掉頁面頂部固定靜態展示和底部靜態展示區域之外,掃描到的文字數量大於1個,我們就認為頁面TTI檢測成功了。

2.3 渲染卡頓和幀率

Google對卡頓定義:介面呈現是指從應用生成幀並將其顯示在螢幕上的動作。要確保使用者能夠流暢地與應用互動,應用呈現每幀的時間不應超過 16ms,以達到每秒 60 幀的呈現速度。如果應用存在介面呈現緩慢的問題,系統會不得不跳過一些幀,這會導致使用者感覺應用不流暢,我們將這種情況稱為卡頓。

2.3.1 卡頓標準

判斷 APP 是否出現卡頓,應該從APP型別是普通應用還是遊戲應用出發,不同型別APP,對應不同的卡頓標準。針對普通型別應用,可以參考借鑑Google 的 Android Vitals 效能指標,針對遊戲,可以參考借鑑騰訊的 PrefDog 效能指標。因為我們APP是普通應用,簡單的介紹下Google Vitals 的卡頓定義。

GoogleVitals把卡頓分為了兩類:

第一類是呈現速度緩慢:在呈現速度緩慢的幀數較多的頁面,當超過 50% 的幀呈現時間超過 16ms 毫秒時,使用者感官明顯示卡頓。

第二類是幀凍結:幀凍結的繪製耗時超過 700ms,為嚴重卡頓問題。

另外,要注意的是,FPS的高低和卡頓沒有必然關係,幀率 FPS 高並不能反映流暢或不卡頓。比如:FPS 為 50 幀,前 200ms 渲染一幀,後 800ms 渲染 49 幀,雖然幀率50,但依然覺得非常卡頓。同時幀率 FPS 低,並不代表卡頓,比如無卡頓時均勻 FPS 為 15 幀。

2.3.2 卡頓量化

當了解卡頓的標準以及原理之後,可以得出結論,只有丟幀情況才能準確判斷是否卡頓。

Flutter官方提供一套基於SchedulerBinding.addTimingsCallback回撥實現的實時幀資料的監控。當flutter 頁面有檢視繪製重新整理時, 系統吐出一串 FrameTiming 資料 ,FrameTiming的資料結構如下:

vsyncStart,

buildStart,

buildFinish,

rasterStart,

rasterFinish

vsyncStart變量表示當前幀繪製的起始時間,buildStart/buildFinish表示WidgetTree的build時間,rasterStart/rasterFinsih表示上屏的光柵化時間,那麼一幀的總渲染時間,可以利用下面公式得到:

totalSpan=>rasterFinish - syncStart

對應Google Android Vitals卡頓的標準:如果一幀totalSpan > 700ms,認為發生了幀凍結,產生了比較嚴重的卡頓;如果1s內,有超過30次的幀的繪製時間totalSpan> 16ms,產生了呈現速度緩慢。

三、流暢度監控方案

在流暢度監控體系中,對於不流暢感知因子,進行單項分析及挖掘,旨在在迭代優化的同時,維持或提升已有的使用者體驗。

監控體系的搭建,分為現狀及優化方向挖掘、監控指標依賴資料補齊、多維度的資料監控、指標監測預警。

監控搭建前期會對於APP現有的效能現狀進行分析,挖掘可優化的方向,初步獲得優化所帶來的預計收益、影響的使用者數等資訊。如:預計使用預載入的方式,來降低使用者的慢載入率,通過各場景的不同使用者操作分析,以及目前客戶端及服務端技術實現的現狀(酒店主服務返回報文大小統計、酒店詳情純前端渲染時間等),來確定慢載入的覆蓋面、觸發時機,以達到更優的效果。

接下來,針對流暢度優化的業務、技改上線的同時,補充對應的監測場景埋點,以支撐流暢度量化衡量的資料,為後續的監控及預警,奠定堅實的基石。

監控體系的核心是大盤及多維度監控的鋪開,大盤資料(如下圖所示),能快速巨集觀的瞭解使用者預訂體驗,明確流暢度提升的進度;而通過各種維度的資料表,即可以找到提升目標,也能監控優化效果。

在實際監控中,會針對不同的指標,設計不同的監控標準,如:慢載入、白屏、奔潰、卡頓等系統因素,除了大盤指標外,還增加了各指標影響佔比、酒店主頁面的報錯率趨勢、版本對比趨勢、報錯機型top分佈等。

對於業務場景比較重的因素,結合業務資料進行分桶等方式的監控,如:詳情頁房型數量關聯TTI耗時分佈、單酒店crash資料等。並與AB實驗系統打通,業務、技改類需求都可以在AB系統中配置流暢度觀測指標,比對業務或技改需求對流暢度的指標影響,作為實驗是否通過的考量指標。

對於各項指標進行單項波動預警,做到有上升就預警、有新增就預警,做到不放過、不遺漏。如:填寫頁業務報錯量(可訂服務、提交訂單、失焦錯誤數),除了對各類報錯率趨勢進行監控外,還會綜合實際使用者流量,區分單項業務報錯的流量大小進行預警,且對拆分多維度(單使用者、單房型等)觸發次數,便於尋找到有特性的badcase,快速定位使用者遇到的問題,挖掘更多的業務優化點。

四、流暢度治理實踐

在APP流暢度治理上,主要從頁面啟動載入速度,長列表卡頓治理,頁面載入閃動三個方面進行了諸多優化實踐,這些優化並沒有涉及高大上的底層引擎優化技術,也沒有複雜的數學理論基礎,更沒有重複造輪子。 我們堅持以資料為導向,用資料驅動方案,用資料驗證方案,發現問題,提出解決方案,解決問題。

4.1 頁面載入速度優化

在頁面載入速度優化上,我們從2021年8月份開始進行迭代優化至今,酒店預訂流程頁面的慢載入率從初始值的42.90%降低至現階段的8.05%。

在頁面啟動載入速度優化上,一般都會採用資料預獲取方案,原理是在上一個頁面提前獲取服務資料,在使用者跳轉到當前頁面時,直接從快取獲取,節省了資料的網路傳輸時間,達到快速展示當前頁面內容的效果。目前在酒店核心預訂流程,都運用了資料預載入技術,如下圖所示:

結合酒店業務特點,資料預載入需要考慮幾個方面問題:第一,酒店預訂流程頁面PV量較高,酒店列表和詳情頁PV在千萬級別。需要考慮資料預載入的時機,避免服務的資源浪費;第二,酒店列表、詳情、訂單填寫頁都有價格資訊,價格資訊對使用者來說是動態資訊,實時都有變價可能,所以需要考慮資料預載入的快取策略,避免因為價格的前後不一致造成使用者誤解。

4.2 Flutter服務通道優化

攜程APP採用的私有服務協議,目前發服務的動作還是在Native程式碼上,而酒店的核心頁面已經轉到了Flutter上。通過Flutter框架提供的通道技術,Native到Flutter的資料傳輸通道需要對資料做一次額外的序列化及反序列化的傳輸,同時傳輸的過程比較耗時,會阻塞UI的渲染主執行緒,對頁面的載入會造成明顯的影響。我們檢測到這個環節之後,和公司的框架團隊一起對Flutter的底層框架進行了改造,可以實現資料流直接的透傳,同時不阻塞UI主執行緒,效能得到了極大的提升。

優化前,通過服務返回的資料流傳遞到Flutter使用,整個過程要經歷以下4步:

① PB反序列化

② Reponse到JsonString的編碼

③ JsonString到Flutter通道傳輸

④ JsonString到Reponse的解碼

整個過程鏈路長,資料傳輸量大,效率低,影響到頁面載入效能,如下圖所示:

改造後,通過服務返回的資料流,直接傳輸到Flutter側,在Flutter直接進行PB的反序列化,傳輸效能得到極大提升。

① PB的資料流Flutter通道傳輸

② PB反序列化到Reponse

整個過程鏈路短,資料傳輸量小,效率高,如下圖所示:

4.3 卡頓問題分析和定位

在 Flutter 中,可以利用效能圖層(Performance Overlay),來分析渲染卡頓問題。如果 UI 產生了卡頓,它可以輔助我們分析並找到原因。如下圖所示:

GPU執行緒的繪製效能情況在圖表的上方,CPU UI執行緒的繪製情況顯示在圖表下方,藍色垂線表示已渲染的幀,綠色色垂線代表的是當前幀。

為了保持60Hz 重新整理頻率,每一幀耗時都應該小於 16ms(1/60 秒)。如果其中有一幀處理時間過長,就會導致介面卡頓,圖表中就會展示出一個紅色豎條。下圖演示了應用出現渲染和繪製耗時的情況下,效能圖層的展示樣式:

如果紅色豎條出現在 GPU 執行緒圖表,意味著渲染的圖形太複雜,導致無法快速渲染;而如果是出現在了 UI 執行緒圖表,則表示 Dart 程式碼消耗了大量資源,需要優化程式碼執行時間。

另外我們可以藉助於AS裡面的Flutter Performance工具檢視Flutter頁面的rendering效能問題,裡面有個很有用的功能Widget rebuild stats,它統計在渲染UI的時候,各個widget rebuild數量情況,可以輔助我們很快的定位存在問題的widget,如下圖:

UI CPU執行緒問題定位

UI執行緒問題實際就是應用效能瓶頸。比如在Widget構建時,在 build 方法中使用了一些複雜運算,或是在Root Isolate 中進行了耗時的同步操作(比如IO)。這些都會明顯增加 CPU 處理時間,造成卡頓。

我們可以使用 Flutter 提供的 Performance 工具,來記錄應用的執行軌跡。Performance 是一個強大的效能分析工具,能夠以時間軸的方式展示 CPU 的呼叫棧和執行時間,去檢查程式碼中可疑的方法呼叫。在點選了Flutter Performance工具欄中的“Open DevTools”按鈕之後,系統會自動開啟 Dart DevTools 的網頁,我們就可以開始分析程式碼中的效能問題了。

GPU問題定位

GPU 問題主要集中在底層渲染耗時上。有時候 Widget 樹雖然構造起來容易,但在 GPU 執行緒下的渲染卻很耗時。涉及 Widget 裁剪、蒙層這類多檢視疊加渲染,或是由於缺少快取導致靜態影象的反覆繪製,都會明顯拖慢 GPU 的渲染速度可以使用效能圖層提供的兩項引數,負責檢查多檢視疊加的檢視渲染開關checkerboardOffscreenLayers和負責檢查快取的影象開關checkerboardRasterCacheImages。

checkerboardOffscreenLayers

多檢視疊加通常會用到 Canvas 裡的 savaLayer 方法,這個方法在實現一些特定的效果(比如半透明)時非常有用,但由於其底層實現會在 GPU 渲染上涉及多圖層的反覆繪製,因此會帶來較大的效能問題。對於 saveLayer方法使用情況的檢查,我們只要在 MaterialApp 的初始化方法中,將 checkerboardOffscreenLayers 開關設定為 true,分析工具就會自動幫我們檢測多檢視疊加的情況了,使用了 saveLayer 的 Widget 會自動顯示為棋盤格式,並隨著頁面重新整理而閃爍。

不過,saveLayer 是一個較為底層的繪製方法,因此我們一般不會直接使用它,而是會通過一些功能性 Widget,在涉及需要剪下或半透明蒙層的場景中間接地使用。所以一旦遇到這種情況,我們需要思考一下是否一定要這麼做,能不能通過其他方式來實現。如下圖所示,因為詳情頭部bar用到高斯模糊,同時使用ClipRRect裁切圓角,ClipRRect會調到savelayer介面,所以該部分產生閃爍。

checkerboardRasterCacheImages

從資源的角度看,另一類非常消耗效能的操作是,渲染影象。這是因為影象的渲染涉及 I/O、GPU 儲存,以及不同通道的資料格式轉換,因此渲染過程的構建需要消耗大量資源。

為了緩解 GPU 的壓力,Flutter 提供了多層次的快取快照,這樣 Widget 重建時就無需重新繪製靜態影象了。與檢查多檢視疊加渲染的checkerboardOffscreenLayers 引數類似,Flutter 也提供了檢查快取影象的開關 checkerboardRasterCacheImages,來檢測在介面重繪時頻繁閃爍的影象(即沒有靜態快取)。

我們可以把需要靜態快取的影象加到 RepaintBoundary 中,RepaintBoundary 可以確定 Widget 樹的重繪邊界,如果影象足夠複雜,Flutter 引擎會自動將其快取,避免重複重新整理。當然,因為快取資源有限,如果引擎認為影象不夠複雜,也可能會忽RepaintBoundary。

4.4 Ctrip React Native(簡稱CRN)頁面的優化

下圖是基本的CRN頁面的載入流程,各個階段的優化之前已有文章進行過描述,如容器預載入,Bundle拆分,容器複用,框架預載入等等在容器層面做了優化。

以酒店訂單填寫頁為例,此頁面採用了CRN的架構,在已有各類容器層面和框架層面的優化之後,我們重點對頁面內重繪做了治理,並將重繪治理做到了極致,主要涉及到上圖中的“5. 首屏首次渲染”和“7. 首屏二次渲染”。

4.4.1 頁面內Action整合

此頁面採用Redux架構,前期經歷了幾年的粗放式開發之後,頁面內的action眾多(Action通過非同步事件的方式觸發狀態管理的改變,從而達到頁面重繪的目的,可以參考Redux的 Action-Reducer-Store模式)。

優化前,如下圖,頁面初始化/開始載入/載入中/載入完成,均觸發多個action,由於action是非同步的,每個資料處理模組都有一些耗時和非同步,載入完成後頁面可能已經重新整理,此處有可能展示了未處理完成的資料,等後續action執行完成後,頁面會再次重新整理。

由於有資料變化,頁面內元素可能會有變化,從而對使用者而言,頁面產生了抖動,同時也會加大JS<=>Native的通訊量,頁面內元素的不斷變化,也會不斷重新整理native中的渲染樹,消耗大量CPU時間,進而導致頁面不流暢,耗時較長。

針對上述情況,我們對頁面內的Action做了整合:

  • 靜態資料避免使用action
  • 觸發時機相同的action儘量合併
  • 非必要資料延遲載入
  • 多層action的更新進行整合

整合後,頁面內的action大致如下:基本只有頁面初始化,主服務返回,以及後續子服務的action了。

在此過程中我們採用了redux-logger的方式來監控action,同時採用MessageQueue的方式來監控action變化觸發重新整理的情況,如下圖:

4.4.2 控制元件重繪治理

為了更好的控制控制元件重繪的頻率,我們對控制元件做了以下拆分:

  • 儘量的拆細元件
  • 降低單檔案的複雜度
  • 元件複用更加方便
  • 依賴資料變少,狀態更好管理
  • 區域性更新資料不影響其他元件
  • 使用Fragments避免多層巢狀

拆分之後元件顆粒度更小,弱業務相關的採用了PureComponent,強業務元件採用Component+shouldComponentUpdate+自行比較屬性是否變化來避免元件的重繪。

通過上述方式的治理,進入填寫頁內已明顯感覺頁面比較輕,主服務返回後頁面立等可重新整理,頁面的渲染速度大幅提升。

重繪治理我們採用了 https:// github.com/welldone-sof tware/why-did-you-render 的方案來檢測元件由於什麼原因重繪,如下圖:

五、規劃和總結

整個APP流暢度治理中,從流暢率從初始47%提升到目前80%,頁面慢載入率從原來的45%降低到現在的8%,白屏率從1.9%降至現在的0.3%,主流程頁面控制元件閃動基本消除,APP效能及使用者體驗有了較明顯的提升。

回顧近半年中文酒店APP流暢度實踐,整個過程艱辛,也時刻伴隨著焦慮。流暢度每一點的進步都不是一蹴而就,輕易達成的。但對整個團隊,收穫滿滿,整個實踐過程中,我們對flutter工程架構做了整體升級,尤其是資料傳輸層改造,業務層邏輯收口等;資料的預載入方案,也從1.0版本升級到2.0版本。最重要的是,整個團隊形成了 資料量化的思想意識和使用者視角出發去優化和解決問題。

目前流暢度2.0的版本也已經落地實踐,2.0將更多的不流暢感知因子加入流暢度統計,如主服務的二次載入,地圖慢載入、圖片及影片慢載入、圖片及影片載入失敗、彈窗及提示資訊等,從更多系統及業務層面來提升使用者的預訂體驗。