經驗總結|我們如何進行Alibaba.com買家效能最佳化?

張哥說技術發表於2023-02-08

經驗總結|我們如何進行Alibaba.com買家效能最佳化?

背景

為什麼要做效能最佳化

對絕大多數網站來說,從宏觀視角上看主要有兩個因素影響最終的網站業務效益:

  1. 使用者的整體規模

  2. 使用者在網站內的轉化率


業務效益 = 使用者規模 * 轉化率
所以為了提升業務效益,需要提升使用者規模以及轉化率。
使用者規模
使用者主要由新使用者和老使用者組成,新老使用者也有各自的流量組成,但無論新老使用者進入網站,都要經過從使用者的客戶端也就是站外落地到我們網站也就是站內這個過程。
尤其我們 Alibaba.com 是一個全球化的網站,從網路設施發達的歐美到欠發達的第三世界國家,都有我們的使用者透過電腦或者手機等裝置訪問我們的網站。
而複雜網路狀況以及技術現狀,會導致網頁載入的緩慢,使用者很可能無法忍受漫長的白屏等待而選擇關閉網站,造成使用者流失。
很多案例也說明了載入效能對使用者規模的影響:

  • At the BBC we’ve noticed that, for every additional second a page takes to load, 10 per cent of users leave.[1]

  • The result of rebuilding our pages for performance led to a 40 percent decrease in Pinner wait time, a 15 percent increase in SEO traffic and a 15 percent increase in conversion rate to signup.[2]

所以網站的載入效能會直接影響到最終落地站內的使用者規模。
轉化率
影響轉化率的因素非常多:網站提供的價值、使用者的意願度以及頁面的使用者體驗都是重要的影響因素。而網站的互動效能,則是使用者體驗的一大影響因素。
如果一個網站開啟過程中,頁面元素各種抖動、滾動頁面時卡頓掉幀,或者滑鼠點選一個按鈕時需要一秒鐘才能響應使用者的互動,都會極大地損害使用者體驗,直接影響使用者的點選率和蹦失率,最終對轉化率造成影響。
所以我們要做效能最佳化。

為什麼總是做效能最佳化

每隔一段時間就要重新提效能最佳化,為什麼:

  • 效能腐化:隨著業務的發展和功能迭代,越來越多的功能會逐步新增到現有的業務中,功能複雜度的增加也會導致效能現狀逐步惡化。

  • 指標的迭代:隨著技術的發展以及對使用者極致體驗的追求,會有更加準確和貼合使用者體感的效能評價指標被提出和使用,老的效能評價指標指導下的最佳化結果在新的指標下並不一定適用。

由於這些原因,我們現有的頁面效能並不是很理想。

目標

我們將效能最佳化的目標定為買家前臺核心瀏覽鏈路頁面滿足 Core Web Vitials 衡量指標下的 Good Url 標準。

什麼是 Core Web Vitials

Core Web Vitials(後面簡稱為 cwv) [3]是 Google 為了衡量頁面效能而提出的有多個指標組成的一套頁面體驗衡量標準,目前主要包含三個指標:LCP, FID, CLS。

  1. LCP: 最大內容繪製,測量載入效能。

  2. FID: 首次輸入延遲,測量互動效能。

  3. CLS: 累計佈局偏移,測量視覺穩定性。

為什麼採用 Core Web Vitials 衡量指標

從上面對 cwv 的解釋可以看到,核心指標從頁面載入速度、使用者輸入效能以及頁面的視覺穩定性三個方向衡量了一個好的頁面效能應該具備的特點,這三個指標也是使用者體驗的核心要素。相較於我們以前的衡量方式 hero element time ,即由每個業務線的開發自己定義一個關鍵頁面元素,以它的渲染完成時間來衡量頁面的效能,cwv 的指標明顯要更加全面,而且更加通用,不易作弊且經受了整個行業的考驗。
下面是 Google 官方對於真實網站在 cwv 指標最佳化下的效能提升情況的統計:

Why page performance matters Studies show that better Core Web Vitals improves user engagement and business metrics. For example:
  • When a site meets the Core Web Vitals thresholds, research showed that users were 24% less likely to abandon page load.
  • With each 100ms reduction in Largest Contentful Paint (LCP), web conversion rate for Farfetch increased by 1.3%.
  • Reducing Cumulative Layout Shift (CLS) by 0.2 led Yahoo! JAPAN to a 15% increase in page views per session, 13% longer session durations, and a 1.72 percentage point decrease in bounce rate.
  • Netzwelt improved Core Web Vitals and saw advertising revenues increase by 18% and page views by 27%.
  • Reducing CLS from 1.65 to 0 significantly uplifted domain rankings globally for redBus.

可以看到無論是 LCP,FID 還是 CLS ,他們的提升都能實實在在的帶來業務的提升,這也是我們要做效能最佳化的原因以及選擇 cwv 作為衡量指標的原因。
更多選擇 cwv 作為衡量指標的原因,可以參考:

  • Why Google chose Core Web Vitals as the new page experience ranking factor[4]

  • 全球化的前端效能度量

所以我們將效能的衡量指標定為 Core Web Vitials,並將目標定為滿足 Good Url 標準,即:

  • LCP < 2500ms
  • FID < 100ms
  • CLS < 0.1


最佳化方法

效能最佳化是一個老生常談的課題,網上有很多現成的方案和方法。當我們直接照搬這些方案或方法時,可能並不一定能快速達到我們的目標。
比如著名的雅虎軍規裡面說:靜態資源儘量分散在多個域名下,以提高瀏覽器併發請求的數量。但隨著技術的發展 HTTP2 的普及,這條方案已經並不適用了。又比如有文章說 js 裡面的陣列遍歷方法 for 比 forEach 要快很多,所以為了更好的效能我們要用 for 來代替 forEach,你花了幾天時間把所有 for 改成了 forEach 結果 cwv 指標根本沒有任何變化。
所以我們要做效能最佳化,不能單純的照搬別人的方案,而是要掌握效能最佳化的方法,正所謂理論指導實踐。對於效能最佳化來說,方法就是:度量、分析和驗證。

度量

如果我們不能衡量頁面現狀,那麼最佳化也就無從談起。
所以效能最佳化的首要事情就是要有度量頁面的效能現狀的方案,為後面的分析和最佳化提供資料支援。
前面提到我們選擇 cwv 作為我們的效能衡量指標,Google 提供了很多 cwv 的檢測方式,包括 PageSpeed Insights、Chrome 開發者工具、搜尋控制檯、web.dev 測量工具等測試工具和平臺。
但它們都有一個問題,不能夠實時檢測線上實際使用者的總體統計值,要麼是隻能本地單次透過類似 PSI(PageSpeed Insights)的測試來看單次評估結果,要麼是類似 Google Search Console 這樣只能在滯後一段時間以後才能看到大量使用者的實際統計值。
而實時的效能資料對我們非常重要,如果沒有實時資料,那麼會有一下問題:

  • 無法快速驗證實驗方案,每上一個方案等兩週再看效果,這效率難以接受。

  • 對於一些大的改造,如果造成了效能的嚴重下跌,卻沒有實時資料的反饋,兩週後再看資料時可能已經對業務造成了很大的損失。

所以我們必須有一套實時觀測效能資料的方案,來指導我們最佳化的策略或者最佳化方向是否正確。
最終我們選擇 web-vitals[6]來獲取 cwv 的值,並在 ICBU 統一效能監控指令碼 big-brother 中新增了相應的打點來上報每個使用者的效能資料並建立了相應的效能監控和報表來實時觀測頁面的效能資料,詳細可以參考:Google Core Web-Vitals 統計&監控
PS:現在可以直接接入 agado[8]來快速獲取更加詳細的效能資料。

分析

結合上一步獲取的效能資料,我們可以系統性的分析當前每一個頁面的效能現狀。知道它現在到底慢不慢,為什麼慢,具體哪裡慢,這樣的話我們也可以針對性的進行最佳化。
拿一個典型的前後端分離的頁面來看,LCP 的組成部分如下:
經驗總結|我們如何進行Alibaba.com買家效能最佳化?
可以看到,從使用者開啟瀏覽器到頁面的 LCP 出現需要經過:建聯、後端響應,HTML 傳輸下載,前端資源下載再到最後的解析渲染,鏈路非常長,導致最終的 LCP 往往離 2500ms 相去甚遠。
如何減少 LCP 的時間,不考慮技術細節的情況下我們不難想到,就是縮短每個耗時組成或者儘可能讓他們並行,這也就是我們最佳化 LCP 的核心宗旨:能減少的減少,不能減少的並行。
從 LCP 的組成上來看,其中重定向、DNS 解析以及建聯和網路傳輸的時間主要受三方或者使用者自身的網路狀況限制,我們能做的並不多。
所以核心的最佳化方向應該聚焦在我們能控制的網路耗時以外的地方,即:

  1. LCP 的時間線組成排布最佳化:即在現有的 LCP 時間線中,哪些可以去掉,哪些可以並行;

  2. LCP 的時間線組成中每一個步驟本身的耗時最佳化:即除了網路耗時以外,後端 RT 以及前端資源下載解析執行時間的最佳化;

對這兩個方向的最佳化我們分別透過:渲染架構最佳化和關鍵渲染路徑最佳化來進行分析最佳化,對於後端 RT 的最佳化本文暫不涉及。

渲染架構上的最佳化

從宏觀視角上來看,渲染架構的選擇是非常重要的,合理的架構選擇可以提高系統的效能下限,為後續進一步最佳化打下基礎。
按照 LCP 最佳化的核心宗旨:能減少的減少,不能減少的並行。從前面 LCP 構成圖上不難看出:

  • 傳統的頁面渲染是不依賴 js 載入的,也就是說這部分時間是有可能省掉的

  • 前端資源的載入和後端響應兩個大的耗時組成之間是序列的關係,有沒有可能讓它們並行

這兩個最佳化方向對應了我們渲染架構上的最佳化方案:同構改造以及流式渲染。

同構改造

渲染架構下價效比最高的最佳化就是同構改造,在有基建支援的情況下可以以較少的人力投入獲取有確定性的比較大的效能提升,一般來說,同構改造可以帶來 500~800ms 的效能提升。
前面提到,我們現在絕大部分業務都是前後端分離的架構。這種架構的優勢是降低了前後端溝通以及釋出維護的成本,兩端可以並行開發獨立維護自己的程式碼庫,穩定性也更高。但這種架構的問題是,由於前後端分離了,後端輸出的 HTML 中只有骨架而沒有內容,到了前端需要再進行一次渲染,這次前端渲染又依賴 js 的下載以及首屏資料的獲取,導致渲染鏈路變長。
能不能把這次額外的渲染的時間省掉呢?答案是可以的,同構直出就是為了解決這個問題。同樣是前後端分離的架構,可以在不損失開發效率以及合作效率的情況下,透過一套 js 程式碼同時跑在前端和後端來實現頁面的直出。
經驗總結|我們如何進行Alibaba.com買家效能最佳化?
這樣就可以省去前端額外的 js 下載以及解析執行渲染的時間,節省下來的時間一般是 500~800ms ,但它的代價就是需要額外維護一個同構服務。
同構服務的話,現在主要有通用同構服務 silkworm-engine(內部自建服務),店鋪自建的同構服務,以及營銷導購的接入集團天馬平臺所提供的同構服務。對於一般業務來說,對接 silkworm-engine 就可以了,對接本身成本也不高。

流式渲染

早在 2010 年,Facebook 的工程師就提出了 bigpipe [9]的頁面渲染方案。透過將一個大的頁面劃分為多個 pagelet 來將漸進式的完成頁面的渲染,核心是為了解決一次性渲染一個大的頁面後端耗時過長的問題。
這個方案思路非常超前,但有一個缺點就是侵入性較大,以我們現有的技術架構下來說不太可能做這樣的改造。但它背後的原理即:http1.1分塊傳輸編碼的特性,可以指導我們來進行成本更低的最佳化,也就是流式渲染,將後端一次性的業務處理 vm 模板渲染並返回分為兩步:

  1. 先返回是不依賴任何業務邏輯的 HTML <head> 部分,瀏覽器拿到後可以先開始進行靜態資源的下載;

  2. 在返回第一段後,同步的去做業務邏輯處理,比如商品資料請求計算等,然後渲染 vm 並返回 HTML 剩餘的 <body> 部分;

經驗總結|我們如何進行Alibaba.com買家效能最佳化?
可以看到在流式渲染的方案下,藍框中 <head> 的下載解析以及首屏 css 等資源的下載和後端的取數計算並行了。
那麼我們就可以節省 Min(前端首屏資源下載時間,後端 RT) 的時間,一般情況下可以有 300~500ms 左右的最佳化,最佳化後的時間線如下:
經驗總結|我們如何進行Alibaba.com買家效能最佳化?
流式渲染的對接也有了工程化的方案:

  • Java 應用參考 流式渲染- Java 端接入文件[10]來進行對接

  • NodeJs 下的 Egg 應用可以參考:Spark:五分鐘接入流式渲染

渲染架構總結

透過同構改造和流式渲染兩個渲染架構的上最佳化,一般情況下都能有 1s 以上的提升。而且不挑業務場景,只要按部就班的做,幾乎所有頁面都可以進行這兩項改造並獲得肉眼可見的增益。
同時除非做了其他渲染架構上的最佳化,一般情況下這 1s 以上的提升不會隨著頁面所在專案的功能迭代而腐化,確保了效能下限。

關鍵渲染路徑最佳化

引用 MDN 上對關鍵渲染路徑(Critical rendering path, 後文簡稱 CRP)的定義:

The Critical Rendering Path is the sequence of steps the browser goes through to convert the HTML, CSS, and JavaScript into pixels on the screen. Optimizing the critical render path improves render performance. The critical rendering path includes the Document Object Model (DOM), CSS Object Model (CSSOM), render tree and layout.

簡單來說就是瀏覽器將 HTML,CSS 和 JavaScript 轉換為螢幕上的畫素所經歷的步驟序列,最佳化包含文件物件模型(DOM),CSS 物件模型 (CSSOM),渲染樹和佈局在內的這些步驟可以提高首屏渲染效能。
CRP 的最佳化涉及到瀏覽器的渲染原理:

  • 瀏覽器首先需要下載並解析 DOM 生成 DOM 樹

  • 在解析的 DOM 的過程中遇到 CSS 檔案進行下載並解析,生成 CSSOM

  • 根據 DOM 樹和 CSSOM 生成渲染樹

  • 根據渲染樹進行佈局

  • 佈局完成後,將具體的內容繪製到螢幕上

當 DOM 或者 CSSOM 發生變化時,比如透過 js 操作了 DOM 節點或者動態插入了 style 標籤等,瀏覽器會重新執行上述步驟。理解了 CRP 的原理,可以看到影響 CRP 的幾個因素:

  • HTML 檔案的下載和解析時間

  • CSS 檔案下載和解析時間

  • 同步 js 檔案的下載和解析

  • DOM 和 CSSOM 的大小

  • 以及佈局和繪製的時間

其中 HTML、CSS 以及同步 js 資源,我們稱其為關鍵資源,因為它們的下載和解析時間會影響到 CRP 的各個階段,這樣我們只要最佳化關鍵資源的下載解析以及 DOM 和 CSSOM 的大小就好了。

  • 對於關鍵資源的最佳化,可以透過縮減關鍵資源的數量,減少資源的大小以及調整關鍵資源的載入順序和快取等來最佳化;

  • 對於 DOM 和 CSSOM 的最佳化,我們可以透過減少 DOM 節點的數量,減少 CSSOM 的大小,以及減少 DOM 和 CSSOM 的層級來最佳化;

快取

可以透過靜態資源快取的方式來減少資源的下載時間,縮短關鍵路徑長度,提高頁面的渲染速度。
具體的方案:

  • 設定資源快取時間

  • 靜態資源走 CDN

  • 動靜分離

靜態資源快取配置

HTTP 快取[12]的具體規範定義本文不過多贅述,簡單講可以將快取分為兩類:

  • 強快取:快取有效期內不發起請求直接使用本地快取的內容

  • 弱快取 or 協商快取:本地快取過期後發起請求和伺服器協商後再使用本地的快取

對於一般的 Assets 應用來說,可以透過在專案根目錄下新增 .assetsmetafile 檔案,來自己指定資源的快取時間,下面的示例是讓瀏覽器以及 CDN (特定廠商)都快取 1 年的配置:

cache-control:max-age=31536000,s-maxage=31536000

這樣,只要資源所在的應用沒有釋出(僅針對非覆蓋式釋出資源,連結沒有發生變化),那麼一年的請求都直接走快取的內容。

上 CDN

眾所周知 CDN 可以透過自己強大的網路和伺服器,將靜態資源快取到離使用者最近的節點上,從而減少資源的下載時間。

CDN 靜態加速

雖然靜態資源上 CDN 是一個常規操作,但本文依然要把靜態資源上 CDN 提出來是因為我們網站仍然有一些類似 <img src="//icbu-cpv-image.oss-us-west-1.aliyuncs.com/Had9f38eda3d942aa9e65ee9198b0660fU.jpg_300x300.jpg">的資源並沒有走 CDN 分發,而是需要使用者每次從源站請求下載資源。

CDN 動態加速

CDN 動態加速這個概念可能不是很常見,它的“動態”是相較於靜態加速中“靜態”來說的,也就是作用的物件的不同。
原理上簡單來說,就是利用 CDN 天然的優勢:有距離使用者近的邊緣節點,並且分佈廣泛。透過 CDN 廠商專門的最佳化,從 CDN 的節點機器中來尋找一條從使用者到源站的更優線路,避免走外網的層層閘道器。從而縮短 CDN 節點於源站之間的網路耗時,使用者的動態請求走 CDN 節點來回源到伺服器,會比使用者自己訪問源伺服器要快很多。
根據我們的統計,動態加速的收益在 200ms+ 。

提高快取命中率

如果釋出頻率過高,由於版本號的變化導致資源對應的 url 頻繁發生變化,那麼本地的快取除了佔用本地的儲存資源外已經失去了它原本的作用。
所以我們需要提高頁面靜態資源的快取命中率,一般提高快取命中率的方法就是動靜分離。
可以透過對頁面依賴的 js,css 進行分層,將公共的依賴比如 React、ReactDOM、Fusion 等打包成一個公共的 bundle 我們稱其為靜態資源,將業務相關的依賴打包成一個業務相關的 bundle 我們稱其為動態資源。
由於靜態資源的釋出頻率是遠遠低於動態資源的釋出頻率的,在絕大多數功能迭代的時候,只會釋出上層業務應用。對於使用者來說就只需要下載動態資源即可,而靜態資源由於沒有變動,所以可以直接從快取中讀取,加快資源載入速度。
這樣就可以提高整體的資源快取命中率,避免整個頁面打一個 js 或者 css bundle 導致的哪怕是一行程式碼的釋出,也需要使用者重新下載整個 js, css bundle 包的問題。

建聯

前面提到著名的雅虎軍規裡面說:靜態資源儘量分散在多個域名下,以提高瀏覽器併發請求的數量。這是由於瀏覽器限制同一個域名下的請求數量,受限於當時 HTTP1.x 下一個 TCP 連線只能處理一個 http 請求的原因,為了能讓瀏覽器能在同一時間對並行請求多個資源而提出的最佳化方案。
到了 HTTP2 [13]時代,一個 TCP 連線就可以並行進行多個請求了,配合 HTTP1.1 的 Connection: Keep-Alive 來進行連線的複用,之前的最佳化方案已經沒有太大的意義了。

域名收攏

甚至由於將資源分佈在了不同的域名下,每個域名的都需要重新進行 DNS 解析、建聯等過程,原本的最佳化手段已經變成了負向最佳化了。
為此需要將分佈在多個不同域名下的地址收斂到一個域名下,減少 DNS 解析的次數並透過連線複用和併發請求獲得更好的效果。
關於更多 HTTP2 的特性,可以參考:HTTP/2 – A protocol for greater performance[14]

提前建連

前面提到 DNS 的解析以及建聯都是非常耗時的操作,為此瀏覽器提供了提前進行 DNS 解析以及建聯的方法,分別是:dns-prefetch[15] 以及 preconnect[16]
dns-prefetch 是 HTML5 新增的一個屬性,用於告訴瀏覽器提前解析某個域名,這樣在瀏覽器需要解析這個域名的時候就不需要再去解析了,可以直接使用快取中的結果。DNS-Prefetch 可以透過兩種方式使用:

<link rel="dns-prefetch" href="

preconnect:告訴瀏覽器提前建立 TCP 連線,DNS 解析以及 TLS 握手等操作,這樣在瀏覽器需要使用這個域名的時候就不需要再去做這些操作了,可以直接使用快取中的結果。在 head 標籤新增如下的 meta 標籤就可以了:

<link rel="preconnect" href="

PS: 需要注意,preconnet 真實建立了連線,屬於比較重的操作,所以僅在關鍵資源對應的域名上使用,否則提前建立過多的連線可能會對頁面造成負向作用。
由於 dns-prefetch 有著比 preconnet 更好的瀏覽器相容性,建議組合 dns-prefetch 以及 preconnet 使用:

<link rel="preconnect" href="https://s.alicdn.com/" crossorigin /><link rel="dns-prefetch" href="

預載入

對於頁面依賴的核心資源,我們可以透過資源預載入的方式來提前下載關鍵資源,從而縮短 CRP 所需時間。效果如下:
經驗總結|我們如何進行Alibaba.com買家效能最佳化?
透過在 html head 中新增 preload[17]的 meta 標籤實現:

  • css: <link rel="preload" href="styles/main.css" as="style"/>

  • js: <link rel="preload" href="main.js" as="script" />

後續需要載入對應的已經預載過的資源的時候就不需要從網路上重新獲取了。

非核心內容非同步

我們網站的內容非常豐富,這意味著功能複雜程式碼體量大。如果一定要在整個首屏載入完畢之後才能使用,那麼使用者體驗就會很差。
為了解決這個問題,我們可以將非核心內容非同步載入,這樣使用者可以在首屏載入完畢之後就可以使用網站的核心功能,而非核心內容則可以在後續載入完畢之後再使用。
方案上,對於使用 webpack 構建程式碼的使用者,可以直接透過 dynamic import [18]來實現。
比如 PPC 的 PC 頁面,我們把左側的篩選進行了非同步載入,以讓使用者能夠更早的看到更為核心的商品 List :





























import React, { lazy } from "react";import SSRCompatibleSuspense from "../../component/ssr-suspense";import { Icon } from "@alifd/next";
function LeftFilterAsync({ data }: { data: PPCSearchResult.PageData }) {  const TrafficLeftFilter = lazy(() =>    // NOTE:頁面的左側篩選,功能複雜,但不屬於使用者首屏需要的內容,所以非同步載入    import(/* webpackChunkName: "left-filter" */ "@alife/traffic-left-filter")  );  const handleChange = (link: string) => {    xxx;  };  return (    <SSRCompatibleSuspense      fallback={        <Icon type="loading" />      }    >      <TrafficLeftFilter        data={data?.snData}        i18n={data?.i18nText}        handleChange={handleChange}      />    </SSRCompatibleSuspense>  );}
export default LeftFilterAsync;

選擇性渲染

當程式碼不能進一步分割,但是渲染的內容有非常多,DOM 節點數量巨大,意味 CRP 的時間。那麼在首屏可以選擇性的渲染一部分更為關鍵的內容,這樣可以減少首屏渲染的時間。
比如 showroom 的無線頁面,首屏後端返回 48 個品的數量,而無線端的手機大部分首屏只能展示 4 個品,所以我們在首屏只渲染前 8 個品,後續的品則在 js ready 後進行二次渲染。
這樣,對於同步直出的頁面,即減少了 html 體積,可以加快下載 html 的速度,同時又減少了首屏 DOM 節點的數量,降低了瀏覽器生成 DOM Tree 的時間。
最終減少 CRP 所需要的時間。

FID 以及 CLS 的最佳化

前面大端的篇幅提到的都是 LCP 相關的最佳化,因為對於面向買家的頁面來說,主要是純展示型頁面,一般沒有非常複雜的互動,所以 FID 和 CLS 的問題不是很凸顯,開發同學在意識上注意一下就好了。

FID

FID: First Input Delay , 從使用者第一次與您的網站互動直到瀏覽器實際能夠對互動作出響應的時間。
這個時間主要是由 js 的執行時間決定的,當瀏覽器的主執行緒執行 js 時,就無法相應使用者的操作,最終影響 FID。那麼最佳化的方向就是:

  • 減少 js 的執行時長

  • 分割 js 中繁重計算任務
減少 js 的執行時長

限制頁面依賴的 js 總體積,可以非常直觀的降低執行時長。
對於頁面的主 js ,透過程式碼分割的方式延遲載入非首屏核心功能的程式碼,只有在程式碼真正要被使用,或者空閒的時候再去載入,將主執行緒空閒出來,從而減少 js 的執行時長。
對於頁面依賴的二方包,一定要嚴格審查,避免同樣的包、依賴比如 React、Fusion 等被重複引入,這樣會導致 js 的體積增大,從而增加執行時長。比如我們某個場景中引入的一個二方 SDK,體積足足 1.6MB,比我們宿主頁面程式碼的 300kb 還大,這裡就有很大的最佳化空間。
控制第三方指令碼的引入,或者儘可能延遲他們的載入,有些投放頁面裡面依賴了很多三方的統計指令碼,這些指令碼的載入會阻塞頁面的渲染,從而影響 FID。

分割 js 中繁重計算任務

多餘繁重的計算任務,可以透過 requestAnimationFrame 的方式,將計算任務分割到多個幀中執行,從而減少 js 的執行時長,給瀏覽器留出更多的空閒時間可以響應使用者的操作。
如果是無法分割的計算任務,可以考慮使用 web worker 的方式,將計算任務放到 worker 中執行,從而不會阻塞主執行緒。

CLS

CLS: Cumulative Layout Shift , 透過計算未在使用者輸入 500 毫秒內發生的佈局偏移的偏移分數總和來測量內容的不穩定性。
導致 CLS 較差的原因:

  • 無尺寸的影像

  • 無尺寸的廣告、嵌入和 iframe

  • 動態注入的內容

  • 導致不可見文字閃爍 (FOIT)/無樣式文字閃爍 (FOUT) 的網路字型

  • 在更新 DOM 之前等待網路響應的操作

在國際站場景,核心需要注意的是:

  • 圖片需要設定具體的寬高而不是 auto ,這樣瀏覽器在圖片下載完成之前就知道該預留多少位置出來;

  • 頁面主體部分儘量同步直出,對於非同步渲染的區塊,做好佔位,不要因為非同步渲染導致頁面跳來跳去;

基本上做到這些,那麼 CLS 就可以達到標準。

策略沉澱

在我們這一年的最佳化過程中,沉澱了一些工具和經驗來幫助我們快速的進行效能最佳化。

工具或系統沉澱

  • agado[19]: 端架構團隊打造的全球化效能度量平臺,幾行程式碼即可高效接入全面的效能度量系統;

  • silkworm engine[20]:買家基礎技術在五年前左右開始搭建的一套通用同構渲染服務,滿足除特定解決方案下(店鋪、天馬等)的頁面快速接入同構能力的訴求;

  • 流式渲染二方庫[21]:快速接入流式渲染的解決方案,並在前期打通了各層代理,後端接入成本可以從3人日減少到小時級別;

取得的成果

SEO 效能最佳化

Google Search Console 後臺的 GoodUrl 比例增長:

  • PC:0 -> 85.9%

  • 無線:0-> 95.1%

具體效能指標(箭頭指向最佳化後的結果):多個場景下頁面的效能都有非常大的提升,CLS 和 FID 全部達到 cwv Good Url 標準,我們著重最佳化的兩個場景中 LCP 也達到 Good Url 標準。
對於 SEO 場景,由於效能提升帶來的搜尋引擎加權,以及更好的到達率,在多個場景下都有 10%~20% 的 uv 增長,大幅提升了使用者規模。

付費頁面最佳化

  • Wap DPA 頁面:LCP 降低了 500ms 左右,業務指標:uv 提升 9% 左右,其他業務指標如點選率、跨端等相關指標也有非常大的提升;

  • PC PLA 頁面:LCP 降低了 900ms 左右,業務指標:核心點選上升 16.1%,uv 增長 4.6%;

  • 以及其他很多還在實驗中的專案,都可以看到業務資料有不同程度的增長;

實際案例

前面說了非常多的理論知識,接下來結合一個實際的例子 —— 無線 showroom (一個 SEO 承接頁)的最佳化,來驗證我們如何根據上面的方法來將一個LCP、FID 以及 CLS 都不達標的頁面最佳化為 Good Url 的。

現狀分析

當我們接手 showroom 的時候,只知道效能很差,因為業務反饋說 Google Search Console 後臺看到我們網站的 Good Url 的達標率為 0%,也就是說 LCP、CLS 以及 FID 都不及格。但具體差到什麼樣子,哪裡差,一無所知。
最佳化的訴求非常迫切,但連現狀是個什麼樣子都不知道,那自然是沒有辦法最佳化的。所以首先要有度量現狀的方案,經過分析和調研,採用了 Google web-vitals 的庫來獲取使用者資料,使用 ICBU 之前就對接 big-brother 來上報採集到的資料,同時透過配置 xflush 來實時展示資料的變動趨勢。
這是最終的效能彙總大盤監控:
經驗總結|我們如何進行Alibaba.com買家效能最佳化?
有了度量方案以後,我們得到了效能的現狀:資料比較差,LCP、CLS 以及 FID 都需要進行最佳化。
有了效能資料以後,再來看一下 showroom 的技術架構:

  • ui 框架採用的 rax + dx 

    • rax 是當時集團的類 react ui 庫

    • dx 是一個能夠解決跨端開發問題的解決方案,主要用於端內 Android 和 iOS 的動態配置,Web 端也有相容展示的方案
  • 渲染鏈路:頁面骨架上了 CDN,採用 js 呼叫非同步介面獲取首屏資料,然後非同步完成頁面的渲染 

    • 由於使用 dx, 需要從後端資料中獲取 dx 模板的字串,然後前端把字串 evel 後作為透過 dx-h5 庫轉化為 rax 元件,再由 rax 渲染到頁面上
  • 樣式檔案:由於採用 rax 和 dx ,最終都是透過內聯樣式實現樣式佈局,沒有獨立的 css 檔案供瀏覽器快取

第一次同構改造

首先看 LCP 的組成:
經驗總結|我們如何進行Alibaba.com買家效能最佳化?
由於頁面非同步渲染的原因,即使 html 上了 CDN 也沒有太大的幫助,整個渲染鏈路拉得非常長,我們接手後先做了同構改造。
由於原本頁面是 rax + dx ,rax 只是空殼,主要內容都是透過 dx 來渲染的, 所以需要對接 dx 的同構。剛好隔壁團隊有一個 dx 的同構服務,我們嘗試對了一下。
首先我們和 Akamai 的同學溝通,將 showroom 的 html 靜態化方案從 CDN 上下了下來,然後前端層面做了一些改造對接了 DX 的同構。
對接完成後,雖然能夠 dx 的同構也能實現直出,但是存在一個很嚴重的問題:服務端資料的 html 結構和前端渲染出來的結構不一樣。
這導致前端 js 的二次渲染頁面會閃動,畢竟 html 結構不一樣,做不到像 react 那樣的 hydrate ,LCP 的時間也變成了二次渲染完成後的時間,同時頁面 FID、CLS 也沒有太大的最佳化。

第二次同構改造

dx 的優勢是一套配置,三端使用。但是在 Web 端實際的操作使用情況下,端內的配置用到 Web 端各種適配問題,最終發展成了一個元件,端內一套 dx 模板配置,端外另一套。也就是說端內外依然是各自獨立維護模板,那麼它的優勢已經沒有了。
同時由於使用了 dx,帶了很多額外的問題:

  • 渲染效率低下:“那麼古爾丹,代價是什麼?” dx 想要解決的是跨端配置的問題,但跨端就要有中間層,每加一層都是成本,導致最終的效率低下, LCP 和 FID 居高不下,之前維護的同學甚至還嘗試過 wasm 的 dx 方案,但依然沒有解決太多的問題。 

經驗總結|我們如何進行Alibaba.com買家效能最佳化?

  • dx 同構端的輸出結構和瀏覽器端不一致,無法進一步的最佳化 

在這些問題下, 我們最終選擇了對 showroom 進行重構,將原本 dx、rax 的方案推到,用 react 進行了重寫。
react 下各種技術方案都相對成熟,重構完成後對接了跑了很多年的 silkworm 的同構服務。react 的同構解決方案非常成熟了,不會出現 dx 下二次渲染頁面需要閃動的問題,由於也沒有膠水層,渲染效率相較於 dx 有了很大的提升。

經驗總結|我們如何進行Alibaba.com買家效能最佳化?

可以看到渲染鏈路簡單了非常多,從監控上看 LCP 降低了 900ms 左右,FID 直接降到了 Good 的標準 100ms 以下。
經驗總結|我們如何進行Alibaba.com買家效能最佳化?

關鍵渲染路徑最佳化

應用拆分

對原本的 showroom 應用進行了拆分,從一個大應用拆分為兩個應用:基礎公共包 traffic-base 以及上層業務應用 traffic-free-wap。
平時的釋出基本都是 traffic-free-wap 的釋出,traffic-base 的釋出頻率很低,提高快取命中率,同時也降低了釋出風險。
但是由於快取的效果是一個長期的影響,短期內看不到 LCP 的變化。

aplus 非同步改造

aplus 是集團打點方案,需要對接一個 aplus.js 的指令碼來實現各種打點尤其是 pv 的上報。原本頁面的 aplus.js 是同步載入和執行的,會阻塞頁面的渲染,所以我們將其改造為非同步載入。
需要修改的是後端服務中的 beacon 模組,在 beacon 模組中的給其中的 script 標籤加上 async 就可以了:

[aplus]aplusKeyUrl=.comaplusKeyUrl=.netaplusKeyUrl=.orgaplusKeyUrl=.cnaplusKeyUrl=.hkaplusKeyUrl=.vipserveraplusLocation=headeraplusCmpType=findaplusFilter=find"iframe_delete=trueaplusFilter=find"at_iframe=1aplusFilter=find"/wangwang/updateaplusUrl=<scriptid="beacon-aplus"asyncsrc="//assets.alicdn.com/g/alilog/??aplus_plugin_icbufront/index.js,mlog/aplus_v2.js"exparams="userid=\#getuid()\#&aplus&ali_beacon_id=\#getcookievalue(ali_beacon_id)\#&ali_apache_id=\#getcookievalue(ali_apache_id)\#&ali_apache_track=\#getcookievalue(ali_apache_track)\#&ali_apache_tracktmp=\#getcookievalue(ali_apache_tracktmp)\#&dmtrack_c={\#getHeaderValue(resin-trace)\#}&pageid=\#getpageid()\#&hn=\#gethostname()\#&asid=\#get_token()\#&treq=\#getHeaderValue(tsreq)\#&tres=\#getHeaderValue(tsres)\#"></script>

aplus 非同步改造上線後,從監控看獲得了 50ms 的 LCP 提升。

預建聯、預載入以及域名收攏

按照我們的最佳化策略,對 showroom 整個頁面的靜態資源域名都收斂到了 s.alicdn.com 下面。
同時對域名進行了 DNS 預解析和建聯:

<link rel="preconnect" href="https://s.alicdn.com" crossorigin /><link rel="dns-prefetch" href="

對於首屏的幾個商品圖,進行了預載入:

<link  rel="preload"  href="https://s.alicdn.com/@sc04/kf/H2df0c8cbb22d49a1b1a2ebdd29cedf05y.jpg_200x200.jpg"  as="image"/><link  rel="preload"  href="https://s.alicdn.com/@sc04/kf/H423ae0f4cf494848bb5c874632270299J.jpg_200x200.jpg"  as="image"/><link  rel="preload"  href="https://s.alicdn.com/@sc04/kf/Hdf15b5c8a7c544c2aee0b2616b2715e3K.jpg_200x200.jpg"  as="image"/>

上線後, 從監控上看 LCP 有了 200ms 左右的提升,穩定在了 Good Url LCP 的邊緣,即將達標。

流式渲染改造

做完以上最佳化後,我們開始進行流式渲染的改造,前後端一起打通了流式渲染的方案,預發測試效果非常好。但是上線後一直不生效,一直排查也排查不出來為什麼。
最後想起來我們走了 Akamai 的動態加速,這意味著使用者的請求還是先打到了 Akamai 的 CDN 上,然後再打到我們的後端服務上。而我們的流式渲染是在後端服務上進行的,所以有可能是 CDN 這裡做了什麼導致我們不生效。
和 CDN 的同學溝通後,發現需要加一個 chunked response streaming 的配置,加上後重新上線測試,流式渲染終於生效了,上線後 LCP 直接降低了 500ms,達到了 CWV Good Url 標準。

CLS 最佳化

LCP 和 CLS 達標以後,只剩下 CLS 還差一點。對於 showroom 來說,導致 cls 的問題主要是 header 的高度不固定,以及商品卡片中圖示的位置不固定。

  • 對於 header 的問題,我們和業務溝通後確定了 header 的高度,並加上了佔位

  • 對於 icon 圖示的問題,針對性的設定固定寬度就可以了

最佳化總結

showroom 的最佳化過程就是根據我們前端提到的最佳化方法,度量->分析->實驗,一步步推進,最終達到了 cwv Good Url 標準,search console 後臺顯示的 good rate 穩定在 90% 以上。
同時由於達到了 Google Good Url 的要求,獲得了 Google 的搜尋權重的加權。按照和 Google 對接的同學提供的衡量方法,wap showroom 的 Clicks +10.6%,Impression +8.8%,相當於額外帶來 10% 左右的 uv。

總結

如果沒有系統性的理論指導,效能最佳化很容易變成一件非常瑣碎的事情,發現 A 解決 A ,發現 B 解決 B,隨著業務迭代也會逐漸腐化,而且如果方向錯誤的話,很容易浪費大量的時間卻拿不到想要的結果。
本文從效能最佳化的理論出發,從度量、分析、驗證三個方面,介紹了我們在效能最佳化中的一些實踐,希望能夠幫助到大家。
雖然目前取得了一些效能和業務上的成果,但是很多頁面比如付費承接依然有很大的最佳化空間,需要持續的最佳化和推進。

參考:

[1]:
[2]:
[3]:https://web.dev/vitals/
[4]:https://webmasters.googleblog.com/2020/05/evaluating-page-experience.html
[6]:
[8]:
[9]:
[10]:
[12]:https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching#private_caches
[13]:https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Evolution_of_HTTP#http2_–_a_protocol_for_greater_performance
[14]:https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Evolution_of_HTTP#http2_–_a_protocol_for_greater_performance
[15]:https://developer.mozilla.org/en-US/docs/Web/Performance/dns-prefetch
[16]:https://developer.mozilla.org/en-US/docs/Web/HTML/Link_types/preconnect
[17]:https://developer.mozilla.org/en-US/docs/Web/HTML/Link_types/preload
[18]:
[19]:
[20]:
[21]:
[22]MDN:https://developer.mozilla.org/en-US/
[23]Web Vitals:https://web.dev/vitals/

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70024923/viewspace-2934367/,如需轉載,請註明出處,否則將追究法律責任。

相關文章