PC首頁資源載入速度由8s降到2s的優化實踐

2023-09-11 06:01:22

隨著需求的不斷開發,前端專案不斷膨脹,業務提出:你們的首頁載入也太慢啦,我都需要7、8秒才能看到內容,於是乎主管就讓我聯合後端開啟優化專項,目標是3s內展示完全首頁的內容。

效能指標

開啟優化時,我們要清晰的知道現狀和目標,以及我們採用什麼樣的手段,通過檢測什麼指標來檢視到優化的過程。

結果指標

根據這個目標,我們可以選擇一些效能指標,google 提供了基於使用者體驗的效能指標,如FCP、LCP、FID、TTI、TBT、CLS等,也有指標更少新的使用者體驗量化方式 Web Vitals,只選取 LCP、FID、CLS。

我們這次主要選擇的指標是 FCPLCP。FCP 表示著使用者能最快看到頁面內容的時間,LCP 則是可視區域的最大內容,這兩個指標代表著使用者真實對於頁面快和慢的體驗,FCP 在 1.8 秒內,LCP 在2.5 秒內是比較好的。

通過 chrome 瀏覽器 lighthouse 功能可檢視的當前頁面的結果指標資料,該專案首頁 LCP 和 FCP 時間分別為 7.5s、1s,LCP 非常之慢了。

過程指標

FCPLCP 是希望達到的結果,在優化的過程中,我們需要一些資料來記錄到底在哪些方向做的優化能導致一個比較好的結果,比如請求數量、頁面載入時間、打包總體積/入口檔案體積、依賴的 cdn 數量、傳輸資源體積。

無痕模式下通過 chrome 開發者工具 network 記錄這些過程指標,當前專案首頁過程指標:請求(requests) 117 個,頁面載入時間(Load)3.79 秒,打包總體積(resources) 21.8MB、依賴的 CDN(需要自己點選頁面右鍵檢視網頁原始碼去數)25個js 、7個css資源,傳輸資源體積(transferred)6.6MB

較多的資源數量、較大的打包體積都導致了頁面載入速度變慢。

頁面生命週期

首先我們得知道頁面的生命週期,才能有針對性的去對前端參與的過程進行優化。那麼從瀏覽器位址列輸入url,到頁面渲染出來,主要經過了哪些步驟呢?

  • 輸入域名後,DNS 域名解析
  • 發起TCP的3次握手
  • 建立TCP連線後發起 http 請求
  • 伺服器端響應http請求,瀏覽器得到html程式碼
  • 瀏覽器解析html程式碼,並請求html程式碼中的資源
  • 瀏覽器對頁面進行渲染呈現給使用者(DOM、CSSOM、渲染樹)

在這個過程中,有很多網路、伺服器參與的過程,而我們主要關注前端能夠進行的優化,比如使用 DNS 預解析、快取機制、減少專案編譯後 html / css/ js 包資源大小。

通用優化

刪除無用程式碼

這一步適用於所有專案,定期的清理掉不需要的程式碼能避免專案無止境的膨脹。因我們專案的歷史遺留問題,存在一些無用的cdn資源、依賴、元件、設定,在頁面載入的過程中他們佔據了一定的網路頻寬。

無用 cdn 資源

當前業務場景下這些資源都已經用不到,也無需在頁面初始化的時候載入。

功能重複的資源

由不同開發者根據集團規範引入了,兩個不同域名但相同功能的前端異常資料採集的 cdn 資源,只需保留一個仍然維護並且推薦使用的即可。

無用依賴

找遍整個專案都用不到的依賴

無用請求邏輯

為了測試元件,跟隨首頁傳送的請求,但因業務場景不太相符,未真正投入使用

無用檔案

可用webpack外掛 unused-files-webpack-plugin 找到已不需要使用的檔案,再通過 nodejs 中內建的檔案處理 fs 定義函數將其遞迴刪除。

網路相關

域名收斂

在專案中,我們可能需要載入很多不同的內容,無論是了為了頁面更為美觀的圖片、字型,還是不適合編譯到專案中體積較大的cdn的資源(如 echarts、谷歌地圖)。

我們知道通過 http 請求獲取資源需要經歷 DNS 域名解析、TCP 三次握手、建立 TCP 連線後傳送 http 請求、伺服器響應 .... 在這裡第一步就是域名解析,如果本地沒有快取,那麼這裡將會從頂級域名開始一層層往下找,需要耗費很多時間。

通過域名收斂,將相同的應用/資源整合到同一域名,減少不同域名之間 DNS 的解析時間,我們專案資源主要有幾大類(xxx代表公司域名)

  • JS、CSS 使用 g.alicdn.comassets.xxx.cn
  • 圖片收斂到檔案平臺 imgcdn.xxx.cnassets.xxx.cn
  • 字型圖示收斂到 at.alicdn.com
  • 特殊如高德地圖 webapi.amap.com 之類

DNS預解析

在執行 js 檔案前會存在一些空閒時間,可以用來解析 dns 地址,dns 預解析是非同步執行的,不會對 html 頁面程式碼造成阻塞,這樣在真正載入資源時可以減少使用者的等待時間。

上面已經將域名進行了收斂,html 中 元素通過 dns- prefetch 的 rel 屬性提供這項功能,然後在 href 屬性中指定要跨域的域名(僅作用於與當前頁面不一樣的域名)。

<link rel="dns-prefetch" href= "//imgcdn.xxx.com">

以上兩個步驟分別刪除無用的資源和提升網路連線,和專案型別、業務場景關聯不大,屬於很多專案都通用的方案。接下來的步驟需要根據資源編譯情況來進行優化。

編譯產物優化

在 webpack 設定中增加外掛 webpack-analyzer-plugin,本地執行專案,編譯後的資源包情況將在在 8888 埠展示。從編譯產物中,我們可以看到每個 js 包原始大小,壓縮後尺寸,js中主要有哪些庫,根據這些具體的資訊進行優化。

圖中 vendor.js 是專案打包的入口檔案,是第一次開啟任意頁面都會載入的資源,仔細觀察那些體積較大的資源,想辦法進行優化。

合併 echarts 版本

從圖中可以看到,繪圖元件有兩個元件庫,BizCharts 、echarts,而 echarts 還分為 5.4 和 4.9 的版本,並且在 index.html 中還通過 cdn 引入了 4.2.1 版本並且沒有使用 externals排除打包。

找到不同 echarts 版本的元件來源,我們專案中兩個版本分別來自 echarts 自身和公司業務元件,還有 react-for-echarts 元件的依賴 。

優化方案是找一個各元件都可以使用版本,將 package.json 和 cdn地址中echarts版本與元件庫中保持一致(使用 5.3.3版本),通過 externals 排除打包,這樣直接使用 cdn 資源,不再將 echarts 編譯至入口檔案 vendor.js 中

// index.html
"externals": {
    "echarts": "echarts"
}

另外還有 BizCharts,因為使用到的選單已下線,所以直接將這部分刪除。

loadash 工具縮包

專案中常會用到 loadash 之類的工具庫,為了使用幾個函數將整個工具庫的資源都引入非常的不值得,比較好的方式是隻通過 import 引入具體需要使用的檔案,如 import _add from 'lodash/fp/add'

但很有可能並不是專案中的每個人都會遵守這樣的規則,如果使用全量引入的方式,如 import _ from 'lodash', 導致全量資源被編譯至專案主入口檔案中。

為了避免這種情況,我們可以使用 babel 外掛來 babel-plugin-lodash 將全量引入方式編譯成按檔案引入。

import _ from 'lodash'
import { add } from 'lodash/fp'

const addOne = add(1)
_.map([1, 2, 3], addOne)

編譯成

import _add from 'lodash/fp/add'
import _map from 'lodash/map'

const addOne = _add(1)
_map([1, 2, 3], addOne)

babel-plugin-lodashbabel 外掛, 在 webpack 中設定到 babel-loader

rules: [
      {
        exclude: /node_modules/,
        test: /\.js$/,
        use: [{
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env'],
            plugins: ['lodash'],
          },
        }],
      },
    ],

我們專案使用的是集團統一腳手架,只暴露出 webpack-chain 的方式去修改,所以是這樣使用。

const merge = require('lodash/merge');
 config.module
  .rule('jsx')
  .use('babel-loader')
  .tap((options) => {
    return merge(options, {
      plugins: ['babel-plugin-lodash'],
    });
  });

從圖中可以看出 xlsx 資源編譯後的體積也非常大,但我們專案當前業務場景已沒有使用到,所以直接刪除,像低頻使用的資源,可以替換成 cdn 連結或者拆包編譯並延遲引入。

通過以上刪除、合併整理,主入口資源從 7.51M 降低 2.51 M。

devtool

藉助 chrome 瀏覽器的 devtool 來進行進行進一步的處理

lighthouse 建議

將專案開啟在 chrome 瀏覽器中,使用 lighthouse 不僅可以用來檢測專案 LCP、FCP評分,還有一些針對當前檢測頁面的建議。

點開之後就是每一條具體的建議,比如提示存在一些首頁用不到的 js、css 資源,為圖片增加寬高減少佈局偏移。

首頁用不到但其它地方需要使用 js、css 資源可以去除入口 index.html 的引入,改為到指定檔案需要使用時再延遲載入 cdn 資源。

方案大概是這樣:

  • 如果指定頁面沒有載入需要的資源,需要通過 scrpt 標籤載入需要的 js 資源,link 標籤載入需要的 css 資源
  • 判斷 cdn 引入的資源已掛載在 window 物件之後,再開始執行頁面的渲染

這樣每個頁面負責自己所需要的資源,無需將所有資源的載入壓力都放到首頁

network 檢視介面響應

效能優化也不是前端努力就足夠的,頁面載入過程中,後端介面響應速度也很關鍵,如果後端介面響應過慢,前端拿不到資料也無法進行渲染。

在我們專案首頁中,獲取使用者選單許可權的介面非常的慢,每次開啟頁面都需要一秒鐘左右才能響應,存在很嚴重的阻塞問題,反饋給後端同學進行優化後,能保持在 100-200毫秒完成響應。

優化結果

通過以上優化,效能過程指標,load 時長、資源體積、依賴cdn數量、傳輸資源體積都有了很大的提升。

效能結果指標 LCP 保持在 1秒,FCP 由 7.5 秒降低至 1.1 秒,頁面渲染完成由 8秒 降到 2秒左右,達到了我的預期。

優化完還有非常重要的事情就是 一定要充分測試! 已上線的需求在進行如此大的改動後,一定要先自測一遍業務功能是否受影響,再提給測試同學排期對整個專案進行測試,等到充分測試完成才能釋出。

總結

攻城容易守城難,做好了一次優化不意味著能夠長期的保持,重要的是全組的同學在平時的需求開發中注意效能做好維護工作,不然資源會像滾雪球一樣越來越大,頁面的載入速度就在無形之中越來越慢。