【最佳實踐】京東小程式-LBS業務場景的效能提升

2023-12-20 15:01:24

一、前言

1.1 京東LBS門詳業務介紹

京東LBS門詳目前已經支援了倉網、藥急送、天選、小時達POP多種業務,並且具備了多端的能力,一套程式碼可以在京東app、健康app、微信小程式中執行,一定程度上研發效率的提升能夠更加快速的支援業務迭代。

隨著業務需求猛增、各種AB場景線上測試,互動複雜度提升,所以對門詳的整體互動體驗,小程式載入速度、列表的捲動效能以及業務資料層面都有更高的要求,因此作為前端研發團隊,我們也迎來了一些新的挑戰。

1.2 面臨的挑戰

基於以上的業務與使用者體驗的要求,門詳急需解決的問題如下:

1.2.1 頁面載入速度緩慢:在中高階機型,實測從點選到店頭渲染資料在1.2s左右【iphone12,小米11青春版】;

1.2.2 門詳列表滑動效能差:

1)和美團外賣、京東到家App原生相比,我們貨架層面不能支援手勢滑動;

2)列表滑動多屏卡死,商品列表載入不流暢;

① 曝光埋點監聽後,DOM元素的查詢方式耗時,阻塞UI執行緒,導致滑動丟幀

② 和美團外賣進行比較,我們的列表沒有聯動載入的功能,給使用者不好的體驗

③ 列表中的分類及商卡資料聯動邏輯複雜,不易維護

④ 列表記憶體佔用高,不釋放

⑤ 二級列表偶現閃動問題

1.2.3 門詳小購物車展開卡頓:加車超過20個品,在中高階機型,出現滑動的明顯示卡頓問題,且開啟購物車緩慢,渲染內容緩慢,在iphone12機型上首次渲染需要2s左右;加車動畫卡頓;

1.2.4 門詳埋點資料不準確:曝光埋點的上報時機和資料產品要求口徑不一致

為更好,更快速的支撐業務訴求,上述的問題解決也迫在眉睫。經過四期的效能優化迭代,我們取得了一定的優化成果。

二、實踐結果

該部分首先重點介紹一下總結果概覽,然後詳細闡述一下頁面啟動和互動結果。

2.1 結果概覽

當我們識別到上述的四類問題之後,進行了整體的問題的分析,識別出的需要高優優化的環節分別是:啟動耗時、互動體驗、能耗優化以及埋點;整體優化完成上線後,我們驗證的結果如下:

① 門詳載入速度整體提升了約50%

同一網路環境下,同款機型優化前後的資料對比如下:

② 小程式健康度評分在90左右;

③ 整體的小程式崩失率減少了**10%**左右;

④ 埋點可用率在**95%**以上;

2.2 啟動結果

① 同5G環境,同門店,機型是iphone14pro,優化前後的效果對比:左側是優化前右側是優化後

② 同5G環境,同門店,機型是iphone14pro,優化後和原生做個對比:左側是京東到家app原生右側是優化後的京東門詳小程式

2.3 互動結果

左側是京東到家原生app,右側是門詳的效果

從上述結果可以看出,在中高階機型上,門詳的開啟速度達到了秒開的效果。接下來詳細介紹門詳效能優化的分析歷程以及解決方案。

三、啟動耗時

下圖是小程式的啟動過程:

影響啟動的四個關鍵因素:

1.引擎預熱耗時:引擎初始化與小程式框架注入耗時;

2.小程式資訊介面耗時:獲取小程式許可權、基本設定資訊、版本等資訊,耗時相對固定;

3.小程式載入耗時:跟業務包大小而變化,包含讀取業務程式碼注入到小程式執行環境以及原生UI初始化耗時;

4.業務初始化耗時:根據業務初始化邏輯與首屏網路介面耗時而變化;

3.1 小程式資訊介面非同步化功能

問題:啟動小程式會同步獲取小程式資訊介面,小程式開啟過程存在耗時。

目標:減少小程式資訊介面請求耗時。

行動:

1.通過將小程式資訊同步介面資訊提前快取;

2.開啟小程式優先使用快取資訊,將同步介面非同步化處理;

結果:介面改為並行後,平均資料減少158ms左右。

3.2 小程式按需載入功能

問題:隨著業務增長,小程式二進位制包達到6MB+(包解壓之後),JS注入執行已經成為耗時主要原因。

目標:降低啟動時業務注入時的程式碼量,從而降低注入耗時。

行動:

1.編譯改造:將渲染層、邏輯層注入內容只保留業務公共框架的部分,頁面的JS程式碼剝離到對應頁面中;

2.啟動改造:啟動時只注入公共框架與當前頁面相關的程式碼,減少注入額外內容;

3.改變包大小【具體操作可見3.3部分門詳包瘦身】

結果:優化前:6MB+ 資料,優化後:注入2.7MB資料, 使用小米11手機測試啟動效能,未使用按需載入功能的啟動時間為:1200ms左右,使用按需載入功能的啟動時間是:800ms左右,效能顯著提升。

3.3 門詳包瘦身

問題:門詳包的大小會直接影響小程式的載入時長

目標:降低包大小,從Release程式碼壓縮包1.2M降低到0.6M以下

行動:

1.分析包體積的構成,檢查不合理的模組,移除冗餘無用的模組;

通過打包工具,檢視包中內容,分析得到包體積較大的模組是 vendors 和 common 模組。

vendors: node_modules 除 Taro 外的公共依賴;

common: 專案中業務程式碼公共邏輯;

taro: node_modules 中 Taro 相關依賴;

runtime: Webpack 執行時入口;

2.逐個模組優化

① 下線掉無用業務模組;

② 優化 vendors 模組,明顯可以看到一些不合理的包被編譯進來了,比如 Axios,小程式的請求一般用jd.request,理論上用不到Axios,此時我們可以開啟 package-lock.json檔案看下,是哪個包依賴了這個模組。例如:@plato這個包應該是在微信域的地址元件,在京東小程式裡使用APP原生元件,所以可以採用端區分的方式process.env.TARO_ENV進行剔除。

重複上述步驟, 檢查 pako lego jsencrypt inversify ,將無用的依賴剔除,優化後的結果,由783KB下降到318KB

③ 優化 common 模組,包括:

common.jxss:經檢查,我們的樣式檔案中有不少icon使用的是base64,會增加包體積,此處我們可以採用雪碧圖方案或者網路圖進行替換。

common.js:對存量程式碼進行優化,減少重複程式碼。此處我們可以藉助eos進行程式碼掃描,檢查到重複程式碼塊進行程式碼合併。

3.制定包瘦身規範,長期控制包大小增量;

① 預防為主

在開發初期就要注重程式碼和資原始檔的優化,避免不必要的程式碼和檔案冗餘,禁止重複造輪子;

資源引入規範:使用到三方功能,需要將必要的功能拆解後獨立參照,媒體資源引入,需以CDN方式,不允許直接放入專案本地;

多端區分規範:合理使用process.env.TARO_ENV​ 進行程式碼剔除,不要引入無用的模組;

優化構建工具:使用或開發構建工具來優化最終包的體積,如webpack 、terser 等工具;

② 防治結合:

定期對小程式進行體積檢查和優化,移除無用程式碼和資源,更新第三方庫到更小體積的版本,以及優化圖片和視訊資源;

定期審查和優化專案依賴,移除不必要或重複的庫和框架;

在程式碼review審查過程中檢查新增程式碼對包體積的影響,避免重複的輪子入庫;

③ 綜合治理:

建立反饋機制,當包體積超標時,能夠及時通知相關人員;

藉助eos掃描,關注程式碼重複率,重複率高則需要及時制定整改方案;

結果:經過上述過程後,包大小從1.2M降低到0.49M。

3.4 應用啟動耗時減少:(首屏渲染 、 介面預請求)

問題:由於我們是基於LBS的門詳業務,首頁介面請求完成後,才能進行頁面的整體渲染,所以首頁主介面請求的耗時時長也成為我們loading時間長的原因之一;另外介面和地址獲取序列的邏輯也加長了整體的資料鏈路;

目標:業務載入時長在中高階機型降低到500ms以內

行動:

① 將主介面的請求進行提前 -> 主介面預熱

一般來說小程式首屏的可互動時間都會受程式碼包注入執行時間影響,在程式碼執行完畢後才能開始業務網路請求,請求資料返回後才能開始真正的內容渲染,為了解決這一痛點,京東小程式Clips是一種通過三執行緒架構實現的特色能力,可以在小程式啟動過程中注入一個輕量化程式碼片段,提前做初始化資料,網路請求等非UI功能,當小程式主環境準備好後,可直接將資料送給邏輯層,實現小程式業務預熱能力。

通過cilp檔案發起預熱請求的功能,以及在小程式範例化後對該請求結果的監聽;達到首頁的介面請求在我們小程式解析的同時,進行了資料的請求,範例化小程式後,在首頁範例完成,準備請求首頁資料時,如果預熱介面已經返回,則直接用預熱的結果,從而達到減少主介面請求的耗時。

② 將地址邏輯前置 -> 範例小程式的同時,獲取地址資訊

結果:序列邏輯修改成並行之後,業務從開始loading 到出現渲染內容,整體時長在iphone12上可以達到350ms左右。

③ 全域性渲染優化

主要原因:

① 跳轉協定、地址資訊、主介面資料都是以Recoil狀態值的方式存在,且顆粒度細碎,大面積多次造成全螢幕重繪。

② 頁面渲染時,樓層多,細粒度狀態觸發頻繁,導致最終整體DOM呈現時間晚,白屏等待時間久。

分析解決:

① 狀態歸類合併,區別狀態和變數

② 分批渲染樓層,區域性渲染

實現方式:

採用全域性Context狀態管理的方式,重新改造了頁面資料監聽方案;從而達到減少多次渲染重繪的效果;

最終實現效果如下圖:

上圖中左側是優化前,使用Recoil狀態繫結元件,改變即變更的渲染方式。

上圖中右側是優化後,使用React Context歸類合併後的資料來源,通過改變Context值,一次性觸發資料更新,並分批渲染。

新的Context將Recoil的狀態改成值的方式,同時通過觀察者模式給予元件訂閱變數更新的能力,儘管Recoil的狀態繫結更方便使用,但是其缺乏變數值的管理,因此可以採用Context彌補這一缺失的能力。

Recoil用法的建議:

1、使用Recoil狀態的地方,一定是JSX中用到的值(狀態變更則觸發元件重新整理),不要在邏輯中使用的變數也使用狀態值;

2、Recoil的顆粒度可以根據獨立的業務模組設定,減少頻繁無效的設定值,物件變更提前做好資料比對;

3、元件只需要改變狀態,不需要依賴狀態時:推薦寫法:const setName = useSetRecoilState(nameAtom);

3.5 整體啟動過程對比示意圖如下:

四、互動體驗

4.1 門詳整體滑動優化

以下是美團外賣、京東到家app門詳和門詳小程式的滑動效果對比分析:

通過分析得出以下3個關鍵互動體驗問題:

① 手勢體驗問題;

② 商品列表列表留白,分類切換不流暢、二級列表偶現閃動的問題;

③ 記憶體佔用高;

4.1.1 整個貨架手勢的支援

前期我們做了支援手勢的調研:

方案對比 movable-view 方案 jds 動態設定樣式方案 scroll-view巢狀
調研結果 拖動效果卡 部分功能暫時不支援,無法獲取自定義元件範例 在內層容器到頂部,內層切換捲動屬性時,會頓一下

根據調研結果綜合評估:我們採用了****scroll-view巢狀的方案,ScrollView巢狀場景下,我們要進行手勢的互動,以及跟手的處理,所以我們面臨了一些挑戰:

① 現存程式碼使用了大量的touch事件攔截,手勢重構成本高;

② 頁面存在多層吸頂和吸底動畫互動效果,需要有合理的替代方案;

原有支援自動吸頂,我們通過動態設定外層scrollTop替換原動畫方案

③ 多層scroll-view巢狀,帶來了多處手勢衝突問題;

在Taro3中小程式邏輯層實現了一套事件系統,包括事件觸發和事件冒泡。在小程式模板中繫結的事件都是以bind的形式。

一般情況下,這套在邏輯層實現的小程式事件系統是可以正常工作的,事件回撥能正確觸發、冒泡、停止冒泡。小程式原生模板中繫結的catchtouchmove事件除了可以阻止回撥函數冒泡觸發外,還能阻止檢視的捲動穿透,但是Taro的事件系統是做不到的。

Taro為我們提供的解決方案:可以為View元件增加catchMove屬性:

// 這個 View 元件會繫結 catchtouchmove 事件而不是 bindtouchmove
<View catchMove={catchMove}></View>


基於以上情況,我們採用動態控制catchmove的方式,來實現是否向上進行事件冒泡通知外層ScrollView。

根據使用者手勢上推和下滑,控制內層ScrollView的scrollY是否可以捲動。

我們在Taro元件中,動態設定catchMove來控制是否向上冒泡,但是在一些場景發現一個問題,已經完成渲染的dom樹在切換catchMove時,DOM元素在檢視內不可見。後經過與Taro團隊溝通,在設定catchMove 時,Taro會使用不同的靜態模版 ,為了解決該問題我們選擇將catchMove 繫結在原生小程式元件上,採用原生和Taro混用解決這個問題。

封裝小程式原生元件:
<view id="container"  catchtouchmove="{{catchMove}}">
  <slot></slot>
</view>


原生元件巢狀Taro元件:
<native-container catchMove={catchMove}>
  巢狀Taro元件
</native-container>


經過我們不斷的調優,各種機型的測試,最後優化前視訊和優化後對比如下:

4.1.2 列表流暢度優化

中低端機型測試:在門詳商品列表中瀏覽xxx個品後,頁面出現卡屏卡死現象;

所以針對上述問題,我們進行了列表的流暢度優化:

曝光元素查詢方式優化

實踐發現曝光createIntersectionObserver回撥獲取元素的方式嚴重影響頁面的滑動流暢度。以下給出兩種寫法:

const categroyNodeObserver = createIntersectionObserver(Taro.getCurrentInstance().page, { observeAll: true })
categroyNodeObserver.observe('#app >>> .className', result => {
    // 1、通過class獲取列表再遍歷查詢對應元素
    const vnodes = document.getElementsByClassName('className') as unknown as HTMLCollectionOf<Element>[];
    const target = vnodes.find(vnode => vnode.uid == result.id)

    // 2、通過id直接獲取元素
    const target = document.getElementById(result.id);
})


通過對比:第2種獲取元素的耗時在個位數毫秒級,第一種耗時高達上百毫秒。

資料結構優化 - 樹結構分類扁平化處理

商品貨架分類是一個巢狀樹結構,最多有三級,如下圖扁平化前黃色區域所示。

在程式碼邏輯中,多處需要獲取末級分類進行邏輯處理,比如:① 點選1級分類,需要找到其下的第一個3級分類進行介面請求;② 點選2級分類,需要找到其下的第一個3級分類進行介面請求;以上兩種情況還有例外:如果沒有3級分類,就會變成使用2級分類進行介面請求。如下圖所示:

未扁平化的資料結構向後查詢下一個分類最大時間複雜度O(n);資料結構扁平化後,將巢狀結構改成單層鏈式結構,如上圖扁平化後綠色區域,後續可以直接在新的資料結構上處理,向後查詢下一個分類最大時間複雜度O(3)。

扁平化遞迴演演算法:

普通遞迴,隨著分類資料的增加,遞迴呼叫棧很多變數開闢了記憶體空間未被釋放,所以要用到尾遞迴。

尾遞迴是一種特殊的遞迴,它的特點是在函數的最後一步呼叫自身,而不是在呼叫後還有其他操作。尾遞迴可以有效地避免棧溢位的風險,因為它不需要儲存每次呼叫的上下文,只需要保留一個棧幀即可。尾遞迴也可以提高遞迴的效能,因為它減少了函數呼叫的開銷。

4.1.3 支援列表補足功能

現有設計方案中,多分類商品無聯動載入邏輯,滑動時有頓挫感,不流暢。

因此,我們在分類扁平化的基礎上,將多分類商品進行連續請求處理,佔滿螢幕,如下圖右側效果。

列表優化過程:

① 新的載入流程如下:

商品列表滑動過程中,分類的聯動選中,我們的做法是:在上述流程圖中的組裝資料環節,在分類末尾位置增加標記位,一條1px的線,然後利用createIntersectionObserver,監聽該元素上推消失和下拉露出的時機,聯動選中對應的1,2,3 級分類。

② 列表分頁載入方式選擇

觸底載入時機試驗對比

監聽Loading的露出來作為下一頁的請求回撥時機,如下圖:

③ 列表分頁渲染層級優化

現狀:採用遞迴的方式 + CustomWrapper 進行渲染列表。有效解決每次setData都是全資料耗時的情況。同時引入一些弊端,其中主要的問題是,頭部排序條的position: sticky,在多分類聯動商卡頁數較多的時候,層級巢狀多層後,sticky的元素會被推出可視區域外。

為了實現setData時,Taro可以增量給小程式傳遞資料,我們回到React底層diff原理,考慮diff可以通過key值優化,於是在Render Row每行元素上增加位置key值,結果:Taro即可進行自動diff,更新單頁資料,這樣DOM結構由原先的巢狀結構變成一層。解決了上述sticky的問題,也提升渲染效能。

載入10頁,每頁setData大約消耗時間:

4.1.4 列表記憶體佔用過高

通過Chrome://inspect效能檢測工具,做了記憶體快照比對發現主要原因有兩點:

① Taro側: 經過記憶體快照對比工具,發現Taro側有兩個問題,一是清空列表前後 Taro 生成的 react fiber 樹(th物件)一直持有快取資料,二是每次setData的資料沒有釋放掉,導致記憶體持續增長。

② 小程式引擎側:在自定義元件更新時,diff前後virtualTree計算出需要更新的節點,對於 jd:if 和 jd:for 標記的節點沒有識別並清理掉。

經過和引擎側和Taro側多次溝通聯調,最終解決上述兩個問題。Taro側問題是升級到3.5.1解決,引擎側是商城App 12.2.4 版本修復。

4.2 門詳二級頁面/購物車卡片/門詳彈窗優化

隨著業務增多,門詳二級頁面的需求量也在不斷迭代,需要支撐業務進行更多品,更多功能的展露,例如加價購、搭配購買等等;所以我們針對各種二級頁面和彈窗卡片也進行了效能上的體驗優化;從中主要解決了以下幾類問題:

① 列表頁面的整體列表抖動問題的修復;

② 二級頁面的渲染時序優化,元件插入方式變更;

③ 小車卡片上大量資料載入的扁平化處理,分頁載入功能的支援;

④ 彈窗功能的整體統一;

二級頁面的詳細優化後續我們會繼續發文。

五、能耗優化

實測:門詳商品列表滑動50頁左右,手機明顯發燙,耗電嚴重,所以高能耗也是我們需要高優解決的問題。

具體分析步驟如下:

① 手機靜止不動,手機發熱、發燙;

② 列表不斷捲動,手機發熱、發燙;

5.1 靜止狀態下的消耗

① 佈局與渲染:說明有dom內容一直在變。如倒計時、banner輪播.

② 媒體與動畫:說明有css在一直執行動畫。如呼吸動畫、loading.

③ javascript:如倒計時事件.

基於以上,我們針對站內的倒計時,頁面的Banner輪播 做了隱藏停止的處理;動畫效果的css樣式由gif圖進行了替換;最後達成效果如下:

能耗的結果:

靜止情況:能耗由高降為低。

在滑動情況下:我們做了捲動防抖和圖片大小優化,滑動瞬時能耗降低了20%,在滑動70頁後開始發熱,在dom節點數量以及節點深度上還有優化的空間,後續我們會繼續分享。

我們總結了能耗優化的一些方向,並放到了我們的開發規範中:

影響能耗的型別 要求
css動畫類 animation xxx 1s infinite; 用完移除,不要在後臺一直執行動畫
setInterval 倒計時 結合requestanimation優化
swiper元件類 視口外停止輪播 加長輪播間隔時間
image元件類 根據視口圖片大小載入適當尺寸的圖片
scroll-view 捲動類 避免節點過多 避免節點層級過深
onScroll回撥 回撥事件防抖 避免複雜邏輯
元素監聽類 監聽及時移除 監聽回撥事件避免做複雜邏輯

六、埋點治理

我們在9月份最後一個版本上線了全站的埋點方案的整改。其中包含底層埋點(PV CLICK EP)埋點引數的邏輯優化,以及所有業務上報埋點的機制與時機處理。整改之前,埋點存在部分問題:

① 曝光埋點的上報時機有誤,不可用;

② PV埋點和點選埋點缺少規範,業務新增埋點不規範,成本高;

③ 曝光請求未聚合,導致頻繁發起埋點請求;

整改之後,我們輸出了門詳的整體埋點開發規範;重新整理了所有曝光埋點的上報時機與整體方案;目前門詳埋點可用率達到95%以上;為業務資料提供了更加準確的方向。

七、總結與展望

以上是我們近期優化的階段性成果,不僅得到了業務和產品同學的認可,而且線上使用者反饋門詳載入慢、卡頓的問題佔比降低了約15%。

使用者體驗提升是需要長期堅持,技術同學需要具備一定的工匠精神,持續探索,我們在規劃中的事情還有:

① 能耗層面的繼續探索與優化;

虛擬列表的支援:目前門店列表支援多分類的載入聯動,且在同一個分類下進行了元件複用與回收;下一步,我們將繼續進行跨分類的元件複用與回收,將門詳列表的回收渲染機制形成統一性的解決方案。

巢狀層級過深導致的效能消耗:進一步減少門詳元件巢狀的問題,從內到外進行整體元件巢狀層級的優化,減緩手機發熱發燙的問題。

② 優化方案同步微信域;

③ 共建能力的支援;

提供門詳元件的共建能力,將門詳元件與業務進行解耦。

④ 小程式對H5的轉化;

⑤ 近原生體驗的提升:視訊能力的支援;

⑥ 小程式側手勢的支援;

期待我們在高效支撐業務迭代的同時,可以給使用者帶來更極致的體驗!!

要特別感謝以下團隊和部門:

京喜與新業務研發部-京東小程式研發團隊,感謝你們在效能優化方面給予的大力支援。

Taro團隊,你們的支援對我們至關重要。

LBS業務產品組-小時購前臺產品團隊,你們的努力讓我們的產品更加完善。

同城研發部-質量提升團隊,你們的專業測試保證了我們服務的高質量。

同城研發部-到家平臺研發組,感謝你們在整理工作中的協助。

你們的幫助對我們的成功至關重要,再次表示衷心的感謝!

作者:京東零售 姜微、鄧樹海、章文順、王冰洋

來源:京東雲開發者社群 轉載請註明來源