手淘店鋪全鏈路性能優(yōu)化
背景
店鋪是導(dǎo)購中重要的一環(huán),承接來自商品詳情頁、主分會(huì)場、主搜等數(shù)十億的流量,店鋪的性能體驗(yàn)就顯得尤為重要。店鋪?zhàn)鳛榱髁看螅軜?gòu)復(fù)雜,形態(tài)多樣,穩(wěn)定性要求高的典型場景,如何針對這類復(fù)雜的場景下做性能上的優(yōu)化是極具挑戰(zhàn)的。店鋪性能優(yōu)化是聯(lián)合客戶端容器團(tuán)隊(duì)、服務(wù)端團(tuán)隊(duì)、前端團(tuán)隊(duì)等多個(gè)團(tuán)隊(duì),諸多團(tuán)隊(duì)協(xié)同合作,共同努力的結(jié)果。過程中我們打通了從容器側(cè)到前端全鏈路的性能埋點(diǎn)采集鏈路,站在全局的鏈路看整個(gè)階段耗時(shí),有針對性的對鏈路進(jìn)行深度優(yōu)化,并通過可視化、多維度直觀呈現(xiàn)性能數(shù)據(jù)。
店鋪架構(gòu)簡介
店鋪是千萬商家的主要運(yùn)營陣地,一方面,不同行業(yè)不同商家對于運(yùn)營的方式有不同的訴求,另一方面,還要滿足KA商家對于品牌的個(gè)性化的表達(dá)。基于此,一個(gè)店鋪里會(huì)同時(shí)存在多個(gè)頁面,并且每個(gè)頁面需要支持商家個(gè)性化裝修。

要解決上述個(gè)性化訴求,我們設(shè)計(jì)了微前端的技術(shù)架構(gòu) + 兩層動(dòng)態(tài)化的方案,方案如下:
微前端架構(gòu):店鋪框架 + 多個(gè)內(nèi)嵌頁 店鋪框架:渲染店鋪的基本信息,管理店鋪Tab 內(nèi)嵌頁:店鋪首頁、寶貝頁、分類頁等 兩層動(dòng)態(tài)化:頁面級動(dòng)態(tài)化 + 組件級動(dòng)態(tài)化 頁面級動(dòng)態(tài)化:一個(gè)店鋪有多個(gè)頁面,不同店鋪有不同的頁面,商家可自主配置 組件級動(dòng)態(tài)化:頁面是多個(gè)模塊組成的,商家可自主裝修
最終店鋪的技術(shù)架構(gòu)如下:

內(nèi)嵌頁是由官方、二方、ISV 提供模塊供給,供商家在后臺(tái)進(jìn)行個(gè)性化裝修,裝修的模塊還可以針對不同人群做個(gè)性化推薦,推薦的內(nèi)容是由算法數(shù)據(jù)決定的。本文重點(diǎn)給大家介紹下店鋪復(fù)雜架構(gòu)性能優(yōu)化是如何做的。
性能采集
為了能直觀的分析性能數(shù)據(jù),我們將用戶點(diǎn)擊到首屏可見看成一個(gè)全鏈路,將大致分為客戶端階段和業(yè)務(wù)邏輯階段,如下:

傳統(tǒng)意義上的性能埋點(diǎn)更多的是局限于前端,但由于我們的程序是運(yùn)行小程序容器之上,容器在啟動(dòng),資源加載,環(huán)境創(chuàng)建之后才開始執(zhí)行業(yè)務(wù)邏輯代碼,容器階段也是整個(gè)鏈路上非常重要的一環(huán),所以我們的階段分析應(yīng)該是全鏈路的。為了得到全鏈路的性能埋點(diǎn),我們聯(lián)合數(shù)據(jù)平臺(tái)定義了性能埋點(diǎn)上報(bào)字段,能將客戶端埋點(diǎn)和業(yè)務(wù)自定義埋點(diǎn)打在一條日志信息中。在這個(gè)基礎(chǔ)之上,其實(shí)前端的工作就變得簡單,只需要使用客戶端封裝的埋點(diǎn)上報(bào)的方式,業(yè)務(wù)方可自定義,上報(bào)性能點(diǎn)位, 前端只需要在需要統(tǒng)計(jì)的階段前后各上報(bào)一個(gè)點(diǎn)位,再將兩個(gè)點(diǎn)位計(jì)算差值,即可計(jì)算階段耗時(shí),業(yè)務(wù)代碼類似下面:
my.call('markPerformance', {
name,
time: Date.now()
});
如上代碼示例:name 是業(yè)務(wù)定義時(shí)間點(diǎn),比如某個(gè)請求開始請求,time 表示該操作執(zhí)行的時(shí)間點(diǎn)。下面就可以將客戶端和業(yè)務(wù)方性能數(shù)據(jù)一起進(jìn)行分析,下面是其中的一個(gè)性能埋點(diǎn)采集, 可以看到 newStatge 是容器上報(bào)的性能埋點(diǎn),performaceMarks 字段就是業(yè)務(wù)自定義上報(bào)的性能埋點(diǎn)

將收集的日志進(jìn)行數(shù)據(jù)加工,然后制作成直觀的數(shù)據(jù)報(bào)表,上報(bào)埋點(diǎn)信息除了關(guān)鍵性能點(diǎn)位以為,還可以拿到設(shè)備,機(jī)型等信息,我們利用這些數(shù)據(jù),產(chǎn)出多個(gè)視角看性能優(yōu)化的圖表,便于我們針對不同場景進(jìn)行性能分析。
分端查看(Android、IOS) 分機(jī)型(低端機(jī)、中端機(jī)、高端機(jī)) 分桶(快速驗(yàn)證實(shí)驗(yàn) AB)

明確了各階段耗時(shí)之后,就可以針對上述性能數(shù)據(jù)做針對性優(yōu)化了, 并也能通過數(shù)據(jù)上的變化的驗(yàn)證優(yōu)化方案的效果,為我們的性能優(yōu)化提供了數(shù)據(jù)保障。
性能優(yōu)化
下圖是站在容器側(cè)角度分析了用戶從點(diǎn)擊到完成首屏渲染其中主要階段。

該階段主要由以下兩部分組成:
容器耗時(shí):URL 攔截和容器創(chuàng)建、元信息加載、框架包(appx 框架)下載 引擎耗時(shí):創(chuàng)建運(yùn)行環(huán)境,初始化對應(yīng)的上下文,加載必要html、css、js文件
在這個(gè)階段中,啟動(dòng)鏈路嚴(yán)重依賴網(wǎng)絡(luò) IO 及 WebView初始化和JS運(yùn)行環(huán)境創(chuàng)建。容器側(cè)主要做了appx 框架預(yù)加載,元信息本地組裝、靜態(tài)插件預(yù)加載、worker和render 預(yù)啟、JSAPI(my.getSystemInfo等近端緩存)
接口優(yōu)化
常規(guī)的優(yōu)化思路便是預(yù)加載和緩存,也是比較萬能的手段,從前面的鏈路圖來看,預(yù)加載和緩存的接口主要是路由接口和店鋪接口,優(yōu)化之后如下圖

如果所示,分為3層優(yōu)化:
CDN緩存:對于裝修接口,因?yàn)檩^少變更,所以直接推到CDN上;只有商家觸發(fā)變更才會(huì)刷新CDN。 本地緩存:把路由接口和店鋪接口做了本地緩存,減少請求的串行時(shí)間 接口預(yù)加載:在路由接口下發(fā)了店鋪接口需要的參數(shù),所以可以實(shí)現(xiàn)店鋪接口的預(yù)加載

另外,雖然使用緩存和端側(cè)提供的數(shù)據(jù)預(yù)取的能力,但依然接口依賴嚴(yán)重,比如上述圖中的算法接口強(qiáng)烈依賴裝修數(shù)據(jù)結(jié)果的返回,這樣計(jì)算首屏的時(shí)間就會(huì)變?yōu)檠b修數(shù)據(jù)接口 + 算法接口,當(dāng)時(shí)設(shè)計(jì)的原因是因?yàn)樗惴ń涌谝蕾囇b修數(shù)據(jù)的返回,拿到裝修數(shù)據(jù)的接口后作為算法接口的入?yún)⒉拍軐⑹灼聊K的算法數(shù)據(jù)請求回來。這也算是阻礙首屏加載的一個(gè)大的問題。我們在想是否可以將串行的邏輯改成并行,最主要要解決的問題是接口參數(shù)需要解耦。
那么如何解耦的呢?優(yōu)化方案是聯(lián)合服務(wù)端,前端添加首屏算法模塊特殊參數(shù),標(biāo)記是首屏模塊算法數(shù)據(jù),將首屏模塊計(jì)算移到服務(wù)端,這樣做到算法接口與裝修數(shù)據(jù)參數(shù)解耦,變成并行加載。優(yōu)化后流程圖如下:

在經(jīng)過上述優(yōu)化后,想更多的利用端上數(shù)據(jù)預(yù)取的能力。于是將裝修數(shù)據(jù)接口和算法接口放到端上預(yù)取。考慮到端上預(yù)取能力是有限度的,并不是能將所有的接口都做到端上預(yù)取,那其實(shí) 4 個(gè)接口(店鋪接口、裝修數(shù)據(jù)接口、降級接口)預(yù)取已經(jīng)將端上預(yù)取能力發(fā)揮的差不多了,考慮到降級接口無參數(shù)依賴,所以將降級接口優(yōu)化為本地存儲(chǔ)+異步更新策略, 主要節(jié)約將網(wǎng)絡(luò)請求轉(zhuǎn)化為緩存時(shí)間,具體實(shí)現(xiàn)流程如下:

并行渲染
前面提過,店鋪是頁面嵌套頁面的技術(shù)架構(gòu),先加載店鋪框架頁,然后加載內(nèi)嵌頁,這個(gè)過程是串行,不管如何優(yōu)化頁面性能,但不能改變的是我們有兩個(gè)頁面的加載,而且是依賴的,那么我們在想是否可以將兩個(gè)頁面渲染變成并行的,答案是可行的。我們針對這個(gè)優(yōu)化主要做的改造點(diǎn)是
店鋪接口下發(fā)首屏內(nèi)嵌頁URL地址: 
容器渲染框架同時(shí)渲染內(nèi)嵌頁
并行渲染帶來的提升非常大,因?yàn)轫撁娴暮臅r(shí)和接口的耗時(shí)完全不是一個(gè)量級;
快照
在性能優(yōu)化過程中,其實(shí)我們還關(guān)注到在用戶點(diǎn)擊到開始執(zhí)行業(yè)務(wù)階段之間,容器階段時(shí)間其實(shí)用戶什么都看不到,出現(xiàn)頁面白屏,那么如何解決客戶端階段白屏問題呢?第一時(shí)間我們會(huì)想到快照技術(shù),但考慮到店鋪數(shù)量千萬家,為每一個(gè)店鋪生成快照文件顯然是不現(xiàn)實(shí)的,因此傳統(tǒng)快照方案對于店鋪場景無法使用。在這種情況下,我們做了基于模板的快照渲染,簡單的理解模板文件 + 真實(shí)數(shù)據(jù) = 真實(shí)快照,得益于在容器階段已經(jīng)將店鋪接口預(yù)取回來后,我們就可以拿到店鋪外框數(shù)據(jù),接下來容器會(huì)運(yùn)行前端提供好的一個(gè)方法,容器在數(shù)據(jù)預(yù)取成功后調(diào)用,返回真實(shí)外框的 DOM 結(jié)構(gòu),直接渲染這份 DOM 數(shù)據(jù)就是快照。
傳統(tǒng)快照渲染 數(shù)據(jù)真實(shí)性無法保證 磁盤占用和命中率成為瓶頸 長尾商家無法享受快照優(yōu)化 基于模板的快照渲染 數(shù)據(jù)真實(shí) 命中率高,磁盤占用率低 對大部分店鋪均適用

本篇文章主要介紹了幾個(gè)主要的優(yōu)化手段,下面對整個(gè)性能優(yōu)化方案總結(jié)如下:
啟動(dòng)前置:woker、render 預(yù)啟 資源預(yù)加載:包括 appx 框架預(yù)加載、靜態(tài)插件預(yù)加載,減小資源請求時(shí)間; 接口優(yōu)化:服務(wù)端合并接口,減少接口請求數(shù)量,合理的利用端上接口預(yù)加載能力,接口串行依賴解耦,串行改并行,不常更新接口合理 CDN 化,添加接口緩存; 插件優(yōu)化: 店鋪會(huì)涉及到非常多的模塊插件包,我們合理的拆包,將使用頻次高的插件靜態(tài)化,提前預(yù)加載,有效的加快了首屏對插件的加載速度; 并行渲染:將框架和內(nèi)嵌頁的串行渲染優(yōu)化為并行渲染,并行渲染的命中率達(dá) 90% 以上; 快照:因每一家店鋪是不一樣的,傳統(tǒng)快照基于快照文件,存在數(shù)據(jù)真實(shí)性無法保證、磁盤占用和命中率成為瓶頸、長尾商家無法享受快照優(yōu)化的缺點(diǎn),因此傳統(tǒng)快照不能滿足店鋪訴求,于是我們探索了一套基于模板文件 + 真實(shí)數(shù)據(jù)的模板快照方案,對所有店鋪普適;

優(yōu)化效果
大盤數(shù)據(jù)
針對大盤數(shù)據(jù)不區(qū)分端首屏可交互數(shù)據(jù)(不包含模塊異步請求和渲染事件)優(yōu)化后是 1.8s 左右

低端機(jī)數(shù)據(jù)

| TMQ 跑分 | 機(jī)型 | 首屏可交互 |
|---|---|---|
| 優(yōu)化前 | Vivo Y67 | 8.5s |
| 優(yōu)化后 | Vivo Y67 | 4.78s |
以下是優(yōu)化后中端機(jī)效果:
總結(jié)與思考

我們從細(xì)化了全鏈路階段耗時(shí)分析,到打通全鏈路性能埋點(diǎn)采集,有針對性對鏈路各個(gè)階段進(jìn)行耗時(shí)分析和優(yōu)化,上線,然后不斷驗(yàn)證,完全可以做為一個(gè)性能優(yōu)化的通用思路,去復(fù)用到其他業(yè)務(wù)場景的性能優(yōu)化。我們堅(jiān)信,店鋪性能體驗(yàn)是一件非常值得持續(xù)投入時(shí)間去做的一件事情,雖然經(jīng)歷了第一階段的優(yōu)化,但距離達(dá)到秒開的體驗(yàn)差距還是很大的,極致的用戶體驗(yàn)是我們的追求,體驗(yàn)優(yōu)化任重道遠(yuǎn),路漫漫其修遠(yuǎn)兮,吾將上下而求索。
內(nèi)推社群
我組建了一個(gè)氛圍特別好的騰訊內(nèi)推社群,如果你對加入騰訊感興趣的話(后續(xù)有計(jì)劃也可以),我們可以一起進(jìn)行面試相關(guān)的答疑、聊聊面試的故事、并且在你準(zhǔn)備好的時(shí)候隨時(shí)幫你內(nèi)推。下方加 winty 好友回復(fù)「面試」即可。
