6s到1s: 雙端應用的秒開最佳化之路

前端黑洞發表於2023-03-01

背景

近來,我們團隊開始嘗試用新的解決方案(雙端開發,同時投放PC端和移動端)解決前端資源短缺問題。但提測後,測試提出我們移動端頁面首屏太慢(6s+),體驗極差。

我嘗試在PC端開啟這個頁面,耗時4s+,載入瀑布流長下面這樣:

20230227202023

粗略一分析,存在下面三個問題:

  • 應用頁面採用了懶載入策略,詳見紅色圈注區域;
  • 入口js,包體積太大,gzip之後還有1.3M, 原始大小4M+
  • 日常靜態資源未開啟cdn

最佳化三部曲

一、應用架構最佳化

在應用架構中,懶載入是一個常見的最佳化技巧,它可以在需要時才載入資源,減少應用的啟動時間和流量消耗。但是,懶載入並不是萬能的,有時候過多的懶載入反而會降低應用效能。因此,在進行應用架構最佳化時,應該根據具體情況權衡是否採用懶載入。

20230227204832

根據瀑布流結合上圖的分析,資源的載入用時4s+,index.html 的時間對於前端來說是不可控的,這完全取決於域名的解析和伺服器的響應。但js 和 css 這一段是完全可控的,我們可以把後面 500ms 的資源載入用時提前,也就是去除懶載入的架構方案。

二、包體積最佳化

包體積最佳化是最佳化應用效能的重要一環。包體積的大小直接影響應用的載入時間和使用者體驗。

當看到1.3M的gzip包大小後,我們第一反應就是去開啟webpack的analyse檢視包體積分佈,結果完全超出想象(這是development模式,註釋了其他頁面入口,僅保留了時段選單一個頁面):
20230227205657

上面這張圖也很直觀的暴露出幾個問題:

  • 同一個chunk同一個依賴被多次打入(以圖中的cook-design為首,見下圖)
  • 包的大小分佈極不均勻
  • 不同chunk打入同一版本依賴(懶載入策略造成)

專案依賴管理,防止多版本

20230227210640

在專案依賴管理中,應該儘可能避免多版本的依賴關係。多版本的依賴關係會導致包體積變大,甚至會產生衝突和錯誤。在日常業務元件開發中,對基礎元件(比如antd)的依賴,可以設定為peerDependencies。

我們的解決思路是透過規範業務的依賴引入方式 和 透過resolution 強制指定版本。

元件庫支援treeShaking

在使用第三方元件庫時,透過treeShaking,可以只載入應用中所需的元件和函式,減少包體積和載入時間。以往我們引入antd3時,為了防止包體積過大,就會引入babel-plugin-import幫助我們去除無用程式碼。但antd4遵從es-module規範,並配置了sideEffect,使得天然支援treeShaking。

我們的專案大量依賴了本地生活的 cook-design 與 cook-design/icons,這兩個由於缺少 treeShaking 配置,導致元件被完整打了進來,其冗餘元件與冗餘元件帶來的依賴,體積在5M左右。

所以我們在和相關的維護者交流了相關想法後,其同意加配置升級支援treeShaking,我們團隊自己的雙端元件庫cook-design-mixin也支援了這個配置。

在最佳化應用架構、最佳化依賴的版本、元件支援treeshaking、以及採用分包策略後,我們的構建結果(production 模式)長下面這樣:

20230227214215

去除無用程式碼

將應用中的無用程式碼去除是包體積最佳化的重要手段之一,我在文章開頭提到,我們這個專案是一個雙端架構的專案, 它是一個編譯時方案,這就是意味著pc端和移動端投放是兩套不同的資源。但由於我們是在一個應用中開發,所以pc端的程式碼和移動端的程式碼是混著寫的,我們透過規範和構建外掛來解決程式碼混寫的問題。

// 下面是一個示例
import { Decorator } from '@alife/wa-mixin-core';
import DMobile from './wa-mobile';
import DDesktop from './wa-desktop';

export default (props: {
  isShowTipStatus: boolean;
  showMode?: boolean;
}) => {
  return Decorator({
    Mobile: <DMobile {...props} />,
    Desktop: <DDesktop />,
  });
};

但透過程式碼分析,我們發現下面兩個問題:

  • 雙端元件庫早期編寫的元件存在大量的編寫不規範,導致構建外掛無法shaking掉另一端的程式碼,從而導致移動端打入了pc端的程式碼(涉及面太廣,後續做專項最佳化)
  • 業務程式碼由於是存量業務(這裡指菜品下發)轉雙端(前期pc端程式碼已開發上線,移動端採用雙端方案開發),所以匯出的不規範,導致包體積劇增

當我們把菜品下發入口加入時,構建結果長下面這樣,比上面單入口的增加2.3M(壓縮後的):
20230227214028

而只留下發一個入口,構建結果長下面這樣:

20230227214606

下面頁面的引入,導致大量的cook-design 與 cook-design/icons被引入,以致於我當時在考慮要不要為icon單獨拆一個包或者透過cdn引入。

包入口被治理後,構建包大小明顯達到了改善:
20230227215027

gzip 之後,大小基本在1M以類(還有改善的空間):

20230227215141

三、網路請求最佳化

網路請求最佳化是最佳化應用效能的另一個重要環節。在進行網路請求最佳化時,可以考慮以下幾個方面:

利用combo降低請求數量

在前端應用中,通常需要載入多個JS、CSS、圖片等資原始檔,這會產生大量的網路請求,影響應用效能。可以使用combo技術,將多個檔案合併成一個檔案請求,減少網路請求的數量, 不過這也需要伺服器的支援。我們的應用模版將react、moment、babel-polyfill等使用cdn作為外部依賴引入,但為了減少請求數,使用了combo服務。

利用http2的多路複用提供請求效率

http2支援多路複用,可以在一個連線上同時傳輸多個請求和響應,提高請求效率和傳輸速度。因此,在使用http2協議時,可以利用多路複用最佳化網路請求。所以才有了上面分多個包的策略,為了能最大化利用現代網路的頻寬。

等待佇列最佳化

20230227220019

在分包依賴完成後,我們發現js資源包的載入存在明顯的等待情況(如上圖),而且是穩定復現的,經過分析,我們發現腳手架提供的應用html模版,存在同步script引入vconsole,從而阻塞了js資源的下載。

減少DNS解析 與 TCP的握手時間

在排查等待佇列問題時,我們還發現,html模版中應用部分外部依賴使用了非集團的cdn域名問題。在http1時代,這個做法是被提倡的,因為瀏覽器對單個域名同時的http連線有限制,所以為了增加並行請求數,在同一個網頁中,會採用幾個cdn域名。但http2,這個就成了束縛,而且採用外部cdn,會帶來安全風險。

在上面的策略全部實施後,日常環境,應用首屏穩定在1.5s左右。待雙端元件庫按規範調整後,這個資料能再快10%。

開啟cdn加速

什麼是cdn,相信大家都應該很瞭解了。在我們應用上線後,靜態資源終於得到了cdn魔法的加持。頁面在我們的投放app掌客端也順利實現了秒開,體驗極好:
20230227222820

總結

首屏最佳化是前端經久不衰的話題,其手段豐富、細節繁多。應用架構最佳化、包體積最佳化和網路請求最佳化是最佳化前端應用效能和提高使用者體驗的關鍵所在。

本文由ChatGPT擬初稿,打工人加工而成。
20230227223210

相關文章